sandeshlauncher 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sandeshlauncher-1.0.0/LICENSE +21 -0
- sandeshlauncher-1.0.0/PKG-INFO +23 -0
- sandeshlauncher-1.0.0/sandeshlauncher/__init__.py +18 -0
- sandeshlauncher-1.0.0/sandeshlauncher/launcher.py +538 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/PKG-INFO +23 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/SOURCES.txt +10 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/dependency_links.txt +1 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/entry_points.txt +2 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/requires.txt +2 -0
- sandeshlauncher-1.0.0/sandeshlauncher.egg-info/top_level.txt +1 -0
- sandeshlauncher-1.0.0/setup.cfg +4 -0
- sandeshlauncher-1.0.0/setup.py +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandesh Bhandari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandeshlauncher
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Minecraft Launcher with auto Java, Fabric support, and profile management
|
|
5
|
+
Author: Sandesh Bhandari
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: customtkinter>=5.2.0
|
|
13
|
+
Requires-Dist: minecraft-launcher-lib>=1.11.0
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Sandesh Bhandari
|
|
3
|
+
|
|
4
|
+
"""Sandesh Minecraft Launcher - A modern Minecraft launcher"""
|
|
5
|
+
|
|
6
|
+
from .launcher import run, launch_game, load_configs, build_ui
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
__author__ = "Sandesh Bhandari"
|
|
10
|
+
__copyright__ = "Copyright (c) 2026 Sandesh Bhandari"
|
|
11
|
+
__license__ = "MIT"
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Entry point for console script"""
|
|
15
|
+
run()
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
run()
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sandesh Launcher – Final Edition
|
|
4
|
+
Offline only. Profiles, version isolation, auto Java, auto Fabric.
|
|
5
|
+
Modern UI, no Microsoft login.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
import subprocess
|
|
12
|
+
import shutil
|
|
13
|
+
import re
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
import customtkinter as ctk
|
|
18
|
+
from tkinter import messagebox
|
|
19
|
+
import minecraft_launcher_lib
|
|
20
|
+
from minecraft_launcher_lib.utils import get_installed_versions
|
|
21
|
+
from minecraft_launcher_lib import fabric
|
|
22
|
+
import jdk
|
|
23
|
+
|
|
24
|
+
# ==================== CONFIG ====================
|
|
25
|
+
DEFAULT_BASE_DIR = os.path.expanduser("~/sandeshlauncher")
|
|
26
|
+
LAUNCHER_FILES_DIR = os.path.join(DEFAULT_BASE_DIR, "launcher_files")
|
|
27
|
+
VERSIONS_DIR = os.path.join(DEFAULT_BASE_DIR, "versions")
|
|
28
|
+
CONFIG_FILE = os.path.join(DEFAULT_BASE_DIR, "launcher_config.json")
|
|
29
|
+
JAVA_DIR = os.path.join(LAUNCHER_FILES_DIR, "java")
|
|
30
|
+
|
|
31
|
+
os.makedirs(LAUNCHER_FILES_DIR, exist_ok=True)
|
|
32
|
+
os.makedirs(VERSIONS_DIR, exist_ok=True)
|
|
33
|
+
os.makedirs(JAVA_DIR, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
ctk.set_appearance_mode("dark")
|
|
36
|
+
ctk.set_default_color_theme("blue")
|
|
37
|
+
|
|
38
|
+
# ==================== GLOBALS ====================
|
|
39
|
+
config = {}
|
|
40
|
+
profiles = {}
|
|
41
|
+
java_path = None
|
|
42
|
+
app = None
|
|
43
|
+
log_textbox = None
|
|
44
|
+
status_label = None
|
|
45
|
+
profile_combobox = None
|
|
46
|
+
|
|
47
|
+
# ==================== HELPERS ====================
|
|
48
|
+
def update_log(message):
|
|
49
|
+
if log_textbox:
|
|
50
|
+
log_textbox.configure(state="normal")
|
|
51
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
52
|
+
log_textbox.insert("end", f"[{timestamp}] {message}\n")
|
|
53
|
+
log_textbox.see("end")
|
|
54
|
+
log_textbox.configure(state="disabled")
|
|
55
|
+
app.update_idletasks()
|
|
56
|
+
|
|
57
|
+
def set_status(text, color="#4CAF50"):
|
|
58
|
+
if status_label:
|
|
59
|
+
status_label.configure(text=text, text_color=color)
|
|
60
|
+
|
|
61
|
+
# ---------- Java ----------
|
|
62
|
+
def get_java_executable():
|
|
63
|
+
global java_path
|
|
64
|
+
if java_path and os.path.exists(java_path):
|
|
65
|
+
return java_path
|
|
66
|
+
java_cmd = shutil.which("java")
|
|
67
|
+
if java_cmd:
|
|
68
|
+
try:
|
|
69
|
+
output = subprocess.check_output([java_cmd, "-version"], stderr=subprocess.STDOUT, text=True)
|
|
70
|
+
if "21" in output or "openjdk version \"21" in output:
|
|
71
|
+
update_log(f"Found Java 21 at: {java_cmd}")
|
|
72
|
+
java_path = java_cmd
|
|
73
|
+
return java_path
|
|
74
|
+
except:
|
|
75
|
+
pass
|
|
76
|
+
update_log("Java 21 not found. Installing OpenJDK 21...")
|
|
77
|
+
set_status("Installing Java...", "#FF9800")
|
|
78
|
+
try:
|
|
79
|
+
installed_path = jdk.install('21', path=JAVA_DIR, jre=False)
|
|
80
|
+
java_path = os.path.join(installed_path, "bin", "java")
|
|
81
|
+
if os.name == 'nt':
|
|
82
|
+
java_path += ".exe"
|
|
83
|
+
if os.path.exists(java_path):
|
|
84
|
+
update_log(f"Java 21 installed at: {java_path}")
|
|
85
|
+
return java_path
|
|
86
|
+
raise Exception("Installation path not found")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
update_log(f"Failed to auto-install Java: {e}")
|
|
89
|
+
messagebox.showerror("Java Error", "Install Java 21 manually.")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# ---------- Minecraft / Fabric ----------
|
|
93
|
+
def is_valid_minecraft_version(version):
|
|
94
|
+
return bool(re.match(r'^\d+(\.\d+){1,2}$', version))
|
|
95
|
+
|
|
96
|
+
def get_installed_version_ids(version_dir):
|
|
97
|
+
try:
|
|
98
|
+
return [v["id"] for v in get_installed_versions(version_dir)]
|
|
99
|
+
except:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
def is_version_installed(version_id, version_dir):
|
|
103
|
+
return version_id in get_installed_version_ids(version_dir)
|
|
104
|
+
|
|
105
|
+
def get_installed_fabric_version(mc_version, version_dir):
|
|
106
|
+
for vid in get_installed_version_ids(version_dir):
|
|
107
|
+
if vid.startswith("fabric-loader") and mc_version in vid:
|
|
108
|
+
return vid
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def delete_corrupted_library(lib_path):
|
|
112
|
+
if os.path.exists(lib_path):
|
|
113
|
+
try:
|
|
114
|
+
os.remove(lib_path)
|
|
115
|
+
update_log(f"Deleted {lib_path}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
update_log(f"Delete failed: {e}")
|
|
118
|
+
|
|
119
|
+
def repair_libraries(version_dir):
|
|
120
|
+
libs_dir = os.path.join(version_dir, "libraries")
|
|
121
|
+
bad = os.path.join(libs_dir, "org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar")
|
|
122
|
+
if os.path.exists(bad):
|
|
123
|
+
delete_corrupted_library(bad)
|
|
124
|
+
update_log("Removed corrupted lwjgl-jemalloc")
|
|
125
|
+
|
|
126
|
+
def ensure_minecraft_installed(version, version_dir, retry=True):
|
|
127
|
+
if not is_valid_minecraft_version(version):
|
|
128
|
+
return False
|
|
129
|
+
try:
|
|
130
|
+
if is_version_installed(version, version_dir):
|
|
131
|
+
return True
|
|
132
|
+
update_log(f"Installing Vanilla {version}...")
|
|
133
|
+
set_status(f"Installing {version}...", "#FF9800")
|
|
134
|
+
minecraft_launcher_lib.install.install_minecraft_version(version, version_dir)
|
|
135
|
+
update_log(f"✓ Vanilla {version} installed")
|
|
136
|
+
return True
|
|
137
|
+
except Exception as e:
|
|
138
|
+
error_msg = str(e)
|
|
139
|
+
if "wrong Checksum" in error_msg and retry:
|
|
140
|
+
update_log("Checksum error, cleaning...")
|
|
141
|
+
match = re.search(r"'(/[^']+\.jar)'", error_msg)
|
|
142
|
+
if match:
|
|
143
|
+
delete_corrupted_library(match.group(1))
|
|
144
|
+
else:
|
|
145
|
+
repair_libraries(version_dir)
|
|
146
|
+
return ensure_minecraft_installed(version, version_dir, retry=False)
|
|
147
|
+
set_status("✗ Vanilla install failed", "#F44336")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def ensure_fabric_installed(mc_version, version_dir):
|
|
151
|
+
if not ensure_minecraft_installed(mc_version, version_dir):
|
|
152
|
+
return None
|
|
153
|
+
existing = get_installed_fabric_version(mc_version, version_dir)
|
|
154
|
+
if existing:
|
|
155
|
+
update_log(f"✓ Fabric already: {existing}")
|
|
156
|
+
return existing
|
|
157
|
+
update_log(f"Installing Fabric for {mc_version}...")
|
|
158
|
+
set_status("Installing Fabric...", "#FF9800")
|
|
159
|
+
try:
|
|
160
|
+
latest = fabric.get_latest_loader_version()
|
|
161
|
+
fabric_id = fabric.install_fabric(mc_version, version_dir, loader_version=latest)
|
|
162
|
+
if not fabric_id:
|
|
163
|
+
raise Exception("install_fabric returned None")
|
|
164
|
+
if not is_version_installed(fabric_id, version_dir):
|
|
165
|
+
found = get_installed_fabric_version(mc_version, version_dir)
|
|
166
|
+
if found:
|
|
167
|
+
fabric_id = found
|
|
168
|
+
else:
|
|
169
|
+
raise Exception("Fabric not found after install")
|
|
170
|
+
update_log(f"✓ Fabric {fabric_id} installed")
|
|
171
|
+
return fabric_id
|
|
172
|
+
except Exception as e:
|
|
173
|
+
set_status("✗ Fabric install failed", "#F44336")
|
|
174
|
+
update_log(f"Fabric error: {e}")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
# ---------- Profile & Config ----------
|
|
178
|
+
def load_configs():
|
|
179
|
+
global config, profiles
|
|
180
|
+
if os.path.exists(CONFIG_FILE):
|
|
181
|
+
try:
|
|
182
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
183
|
+
config = json.load(f)
|
|
184
|
+
except:
|
|
185
|
+
config = get_default_config()
|
|
186
|
+
else:
|
|
187
|
+
config = get_default_config()
|
|
188
|
+
profiles = config.get("profiles", {})
|
|
189
|
+
if not profiles:
|
|
190
|
+
profiles["Default"] = get_default_profile()
|
|
191
|
+
config["profiles"] = profiles
|
|
192
|
+
save_configs()
|
|
193
|
+
|
|
194
|
+
def get_default_config():
|
|
195
|
+
return {"base_dir": DEFAULT_BASE_DIR, "profiles": {}}
|
|
196
|
+
|
|
197
|
+
def get_default_profile():
|
|
198
|
+
return {
|
|
199
|
+
"username": "Player",
|
|
200
|
+
"version": "1.21.1",
|
|
201
|
+
"loader": "vanilla",
|
|
202
|
+
"ram": "2048",
|
|
203
|
+
"width": "854",
|
|
204
|
+
"height": "480",
|
|
205
|
+
"fullscreen": False
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def save_configs():
|
|
209
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
210
|
+
json.dump(config, f, indent=4)
|
|
211
|
+
update_log("Settings saved")
|
|
212
|
+
|
|
213
|
+
def refresh_profile_list():
|
|
214
|
+
if profile_combobox:
|
|
215
|
+
profile_combobox.configure(values=list(profiles.keys()))
|
|
216
|
+
if profiles:
|
|
217
|
+
profile_combobox.set(list(profiles.keys())[0])
|
|
218
|
+
|
|
219
|
+
# ---------- Profile dialogs ----------
|
|
220
|
+
def add_profile():
|
|
221
|
+
def save_new():
|
|
222
|
+
name = profile_name_entry.get().strip()
|
|
223
|
+
if not name or name in profiles:
|
|
224
|
+
messagebox.showerror("Error", "Invalid or duplicate name")
|
|
225
|
+
return
|
|
226
|
+
profiles[name] = {
|
|
227
|
+
"username": username_entry.get().strip() or "Player",
|
|
228
|
+
"version": version_entry.get().strip() or "1.21.1",
|
|
229
|
+
"loader": loader_var.get(),
|
|
230
|
+
"ram": ram_entry.get().strip() or "2048",
|
|
231
|
+
"width": width_entry.get().strip() or "854",
|
|
232
|
+
"height": height_entry.get().strip() or "480",
|
|
233
|
+
"fullscreen": fullscreen_var.get()
|
|
234
|
+
}
|
|
235
|
+
save_configs()
|
|
236
|
+
refresh_profile_list()
|
|
237
|
+
new_window.destroy()
|
|
238
|
+
update_log(f"Profile '{name}' added")
|
|
239
|
+
new_window = ctk.CTkToplevel(app)
|
|
240
|
+
new_window.title("Add Profile")
|
|
241
|
+
new_window.geometry("450x550")
|
|
242
|
+
new_window.update_idletasks()
|
|
243
|
+
new_window.grab_set()
|
|
244
|
+
main = ctk.CTkFrame(new_window, fg_color="transparent")
|
|
245
|
+
main.pack(fill="both", expand=True, padx=20, pady=20)
|
|
246
|
+
ctk.CTkLabel(main, text="Create Profile", font=("Arial", 18, "bold")).pack(pady=(0,15))
|
|
247
|
+
ctk.CTkLabel(main, text="Profile Name").pack(anchor="w")
|
|
248
|
+
profile_name_entry = ctk.CTkEntry(main, width=350)
|
|
249
|
+
profile_name_entry.pack(pady=(0,10), fill="x")
|
|
250
|
+
ctk.CTkLabel(main, text="Username").pack(anchor="w")
|
|
251
|
+
username_entry = ctk.CTkEntry(main, width=350)
|
|
252
|
+
username_entry.pack(pady=(0,10), fill="x")
|
|
253
|
+
ctk.CTkLabel(main, text="Minecraft Version").pack(anchor="w")
|
|
254
|
+
version_entry = ctk.CTkEntry(main, width=350, placeholder_text="1.21.1")
|
|
255
|
+
version_entry.pack(pady=(0,10), fill="x")
|
|
256
|
+
ctk.CTkLabel(main, text="Mod Loader").pack(anchor="w")
|
|
257
|
+
loader_var = ctk.StringVar(value="vanilla")
|
|
258
|
+
ctk.CTkOptionMenu(main, values=["vanilla", "fabric"], variable=loader_var).pack(pady=(0,10), fill="x")
|
|
259
|
+
ctk.CTkLabel(main, text="RAM (MB)").pack(anchor="w")
|
|
260
|
+
ram_entry = ctk.CTkEntry(main, width=350, placeholder_text="2048")
|
|
261
|
+
ram_entry.pack(pady=(0,10), fill="x")
|
|
262
|
+
size_frame = ctk.CTkFrame(main, fg_color="transparent")
|
|
263
|
+
size_frame.pack(fill="x", pady=(0,10))
|
|
264
|
+
ctk.CTkLabel(size_frame, text="Width:").pack(side="left", padx=(0,5))
|
|
265
|
+
width_entry = ctk.CTkEntry(size_frame, width=80, placeholder_text="854")
|
|
266
|
+
width_entry.pack(side="left", padx=(0,15))
|
|
267
|
+
ctk.CTkLabel(size_frame, text="Height:").pack(side="left", padx=(0,5))
|
|
268
|
+
height_entry = ctk.CTkEntry(size_frame, width=80, placeholder_text="480")
|
|
269
|
+
height_entry.pack(side="left")
|
|
270
|
+
fullscreen_var = ctk.BooleanVar(value=False)
|
|
271
|
+
ctk.CTkCheckBox(main, text="Fullscreen", variable=fullscreen_var).pack(anchor="w", pady=(0,15))
|
|
272
|
+
btn_frame = ctk.CTkFrame(main, fg_color="transparent")
|
|
273
|
+
btn_frame.pack(fill="x")
|
|
274
|
+
ctk.CTkButton(btn_frame, text="Cancel", command=new_window.destroy, fg_color="#555").pack(side="right", padx=5)
|
|
275
|
+
ctk.CTkButton(btn_frame, text="Create", command=save_new, fg_color="#4CAF50").pack(side="right", padx=5)
|
|
276
|
+
|
|
277
|
+
def edit_profile():
|
|
278
|
+
selected = profile_combobox.get()
|
|
279
|
+
if not selected:
|
|
280
|
+
return
|
|
281
|
+
p = profiles[selected]
|
|
282
|
+
def save_edits():
|
|
283
|
+
profiles[selected] = {
|
|
284
|
+
"username": username_entry.get().strip() or "Player",
|
|
285
|
+
"version": version_entry.get().strip() or "1.21.1",
|
|
286
|
+
"loader": loader_var.get(),
|
|
287
|
+
"ram": ram_entry.get().strip() or "2048",
|
|
288
|
+
"width": width_entry.get().strip() or "854",
|
|
289
|
+
"height": height_entry.get().strip() or "480",
|
|
290
|
+
"fullscreen": fullscreen_var.get()
|
|
291
|
+
}
|
|
292
|
+
save_configs()
|
|
293
|
+
edit_window.destroy()
|
|
294
|
+
update_log(f"Profile '{selected}' updated")
|
|
295
|
+
edit_window = ctk.CTkToplevel(app)
|
|
296
|
+
edit_window.title(f"Edit {selected}")
|
|
297
|
+
edit_window.geometry("450x550")
|
|
298
|
+
edit_window.update_idletasks()
|
|
299
|
+
edit_window.grab_set()
|
|
300
|
+
main = ctk.CTkFrame(edit_window, fg_color="transparent")
|
|
301
|
+
main.pack(fill="both", expand=True, padx=20, pady=20)
|
|
302
|
+
ctk.CTkLabel(main, text=f"Editing {selected}", font=("Arial", 18, "bold")).pack(pady=(0,15))
|
|
303
|
+
ctk.CTkLabel(main, text="Username").pack(anchor="w")
|
|
304
|
+
username_entry = ctk.CTkEntry(main, width=350)
|
|
305
|
+
username_entry.insert(0, p.get("username", "Player"))
|
|
306
|
+
username_entry.pack(pady=(0,10), fill="x")
|
|
307
|
+
ctk.CTkLabel(main, text="Minecraft Version").pack(anchor="w")
|
|
308
|
+
version_entry = ctk.CTkEntry(main, width=350)
|
|
309
|
+
version_entry.insert(0, p.get("version", "1.21.1"))
|
|
310
|
+
version_entry.pack(pady=(0,10), fill="x")
|
|
311
|
+
loader_var = ctk.StringVar(value=p.get("loader", "vanilla"))
|
|
312
|
+
ctk.CTkOptionMenu(main, values=["vanilla", "fabric"], variable=loader_var).pack(pady=(0,10), fill="x")
|
|
313
|
+
ctk.CTkLabel(main, text="RAM (MB)").pack(anchor="w")
|
|
314
|
+
ram_entry = ctk.CTkEntry(main, width=350)
|
|
315
|
+
ram_entry.insert(0, p.get("ram", "2048"))
|
|
316
|
+
ram_entry.pack(pady=(0,10), fill="x")
|
|
317
|
+
size_frame = ctk.CTkFrame(main, fg_color="transparent")
|
|
318
|
+
size_frame.pack(fill="x", pady=(0,10))
|
|
319
|
+
ctk.CTkLabel(size_frame, text="Width:").pack(side="left", padx=(0,5))
|
|
320
|
+
width_entry = ctk.CTkEntry(size_frame, width=80)
|
|
321
|
+
width_entry.insert(0, p.get("width", "854"))
|
|
322
|
+
width_entry.pack(side="left", padx=(0,15))
|
|
323
|
+
ctk.CTkLabel(size_frame, text="Height:").pack(side="left", padx=(0,5))
|
|
324
|
+
height_entry = ctk.CTkEntry(size_frame, width=80)
|
|
325
|
+
height_entry.insert(0, p.get("height", "480"))
|
|
326
|
+
height_entry.pack(side="left")
|
|
327
|
+
fullscreen_var = ctk.BooleanVar(value=p.get("fullscreen", False))
|
|
328
|
+
ctk.CTkCheckBox(main, text="Fullscreen", variable=fullscreen_var).pack(anchor="w", pady=(0,15))
|
|
329
|
+
btn_frame = ctk.CTkFrame(main, fg_color="transparent")
|
|
330
|
+
btn_frame.pack(fill="x")
|
|
331
|
+
ctk.CTkButton(btn_frame, text="Cancel", command=edit_window.destroy, fg_color="#555").pack(side="right", padx=5)
|
|
332
|
+
ctk.CTkButton(btn_frame, text="Save", command=save_edits, fg_color="#2196F3").pack(side="right", padx=5)
|
|
333
|
+
|
|
334
|
+
def delete_profile():
|
|
335
|
+
sel = profile_combobox.get()
|
|
336
|
+
if sel and messagebox.askyesno("Confirm", f"Delete '{sel}'?"):
|
|
337
|
+
del profiles[sel]
|
|
338
|
+
save_configs()
|
|
339
|
+
refresh_profile_list()
|
|
340
|
+
update_log(f"Profile '{sel}' deleted")
|
|
341
|
+
|
|
342
|
+
# ---------- Launch ----------
|
|
343
|
+
def launch_game():
|
|
344
|
+
profile_name = profile_combobox.get()
|
|
345
|
+
if not profile_name:
|
|
346
|
+
messagebox.showerror("Error", "No profile")
|
|
347
|
+
return
|
|
348
|
+
profile = profiles.get(profile_name)
|
|
349
|
+
if not profile:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
username = profile.get("username", "Player")
|
|
353
|
+
mc_version = profile.get("version", "1.21.1")
|
|
354
|
+
loader = profile.get("loader", "vanilla")
|
|
355
|
+
ram = profile.get("ram", "2048")
|
|
356
|
+
width = profile.get("width", "854")
|
|
357
|
+
height = profile.get("height", "480")
|
|
358
|
+
fullscreen = profile.get("fullscreen", False)
|
|
359
|
+
|
|
360
|
+
# Instance folder
|
|
361
|
+
loader_str = "Fabric" if loader == "fabric" else "Vanilla"
|
|
362
|
+
inst_name = f"{profile_name}-{mc_version}-{loader_str}".replace(" ", "_")
|
|
363
|
+
game_dir = os.path.join(VERSIONS_DIR, inst_name)
|
|
364
|
+
os.makedirs(game_dir, exist_ok=True)
|
|
365
|
+
for folder in ["mods", "config", "saves", "resourcepacks", "shaderpacks", "tmp"]:
|
|
366
|
+
os.makedirs(os.path.join(game_dir, folder), exist_ok=True)
|
|
367
|
+
|
|
368
|
+
java_exe = get_java_executable()
|
|
369
|
+
if not java_exe:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
ram_mb = int(ram)
|
|
374
|
+
if ram_mb < 512:
|
|
375
|
+
ram_mb = 512
|
|
376
|
+
except:
|
|
377
|
+
ram_mb = 2048
|
|
378
|
+
|
|
379
|
+
# Offline auth
|
|
380
|
+
auth_data = {
|
|
381
|
+
"username": username,
|
|
382
|
+
"uuid": str(uuid.uuid4()),
|
|
383
|
+
"token": "0"
|
|
384
|
+
}
|
|
385
|
+
update_log(f"Offline mode: {username}")
|
|
386
|
+
|
|
387
|
+
# Install required version
|
|
388
|
+
actual_version = mc_version
|
|
389
|
+
if loader == "fabric":
|
|
390
|
+
fabric_ver = ensure_fabric_installed(mc_version, game_dir)
|
|
391
|
+
if not fabric_ver:
|
|
392
|
+
set_status("✗ Fabric setup failed", "#F44336")
|
|
393
|
+
return
|
|
394
|
+
actual_version = fabric_ver
|
|
395
|
+
else:
|
|
396
|
+
if not ensure_minecraft_installed(mc_version, game_dir):
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Launch
|
|
400
|
+
try:
|
|
401
|
+
set_status("🚀 Launching...", "#FF9800")
|
|
402
|
+
update_log(f"Launching {actual_version} as {username}")
|
|
403
|
+
jvm_args = [f"-Xmx{ram_mb}M", f"-Xms{max(512, ram_mb // 2)}M",
|
|
404
|
+
f"-Djava.io.tmpdir={os.path.join(game_dir, 'tmp')}"]
|
|
405
|
+
game_args = []
|
|
406
|
+
if width and height:
|
|
407
|
+
game_args.extend(["--width", str(width), "--height", str(height)])
|
|
408
|
+
if fullscreen:
|
|
409
|
+
game_args.append("--fullscreen")
|
|
410
|
+
options = {
|
|
411
|
+
"username": auth_data["username"],
|
|
412
|
+
"uuid": auth_data["uuid"],
|
|
413
|
+
"token": auth_data["token"],
|
|
414
|
+
"jvmArguments": jvm_args,
|
|
415
|
+
"gameArguments": game_args,
|
|
416
|
+
"executable": java_exe
|
|
417
|
+
}
|
|
418
|
+
command = minecraft_launcher_lib.command.get_minecraft_command(actual_version, game_dir, options)
|
|
419
|
+
process = subprocess.Popen(command)
|
|
420
|
+
set_status(f"✓ Game started (PID: {process.pid})", "#4CAF50")
|
|
421
|
+
update_log("✓ Minecraft launched")
|
|
422
|
+
except Exception as e:
|
|
423
|
+
set_status("✗ Launch failed", "#F44336")
|
|
424
|
+
update_log(f"Error: {e}")
|
|
425
|
+
|
|
426
|
+
# ---------- UI ----------
|
|
427
|
+
def build_ui():
|
|
428
|
+
global app, log_textbox, status_label, profile_combobox
|
|
429
|
+
app = ctk.CTk()
|
|
430
|
+
app.title("Sandesh Launcher")
|
|
431
|
+
app.geometry("1000x700")
|
|
432
|
+
app.minsize(800, 600)
|
|
433
|
+
|
|
434
|
+
app.grid_rowconfigure(0, weight=1)
|
|
435
|
+
app.grid_columnconfigure(0, weight=0)
|
|
436
|
+
app.grid_columnconfigure(1, weight=1)
|
|
437
|
+
|
|
438
|
+
# Sidebar
|
|
439
|
+
sidebar = ctk.CTkFrame(app, width=260, corner_radius=0, fg_color="#1e1e1e")
|
|
440
|
+
sidebar.grid(row=0, column=0, sticky="nsew")
|
|
441
|
+
sidebar.grid_propagate(False)
|
|
442
|
+
|
|
443
|
+
ctk.CTkLabel(sidebar, text="⛏️", font=("Arial", 48)).pack(pady=(30,0))
|
|
444
|
+
ctk.CTkLabel(sidebar, text="SANDESH LAUNCHER", font=("Arial", 18, "bold")).pack()
|
|
445
|
+
ctk.CTkFrame(sidebar, height=2, fg_color="#333").pack(fill="x", padx=20, pady=10)
|
|
446
|
+
|
|
447
|
+
# Profiles
|
|
448
|
+
profile_frame = ctk.CTkFrame(sidebar, fg_color="transparent")
|
|
449
|
+
profile_frame.pack(fill="x", padx=20, pady=10)
|
|
450
|
+
ctk.CTkLabel(profile_frame, text="PROFILES", font=("Arial", 12, "bold"), text_color="#aaa").pack(anchor="w")
|
|
451
|
+
profile_combobox = ctk.CTkComboBox(profile_frame, values=list(profiles.keys()), state="readonly", height=35)
|
|
452
|
+
profile_combobox.pack(fill="x", pady=(5,10))
|
|
453
|
+
if profiles:
|
|
454
|
+
profile_combobox.set(list(profiles.keys())[0])
|
|
455
|
+
|
|
456
|
+
btn_frame = ctk.CTkFrame(profile_frame, fg_color="transparent")
|
|
457
|
+
btn_frame.pack(fill="x")
|
|
458
|
+
ctk.CTkButton(btn_frame, text="➕ Add", command=add_profile, fg_color="#4CAF50", height=32).pack(side="left", padx=(0,5), fill="x", expand=True)
|
|
459
|
+
ctk.CTkButton(btn_frame, text="✏️ Edit", command=edit_profile, fg_color="#2196F3", height=32).pack(side="left", padx=5, fill="x", expand=True)
|
|
460
|
+
ctk.CTkButton(btn_frame, text="🗑️ Delete", command=delete_profile, fg_color="#F44336", height=32).pack(side="left", padx=(5,0), fill="x", expand=True)
|
|
461
|
+
|
|
462
|
+
ctk.CTkFrame(sidebar, height=2, fg_color="#333").pack(fill="x", padx=20, pady=20)
|
|
463
|
+
|
|
464
|
+
info_text = """📁 Each profile's game data is isolated.
|
|
465
|
+
|
|
466
|
+
~/sandeshlauncher/versions/ProfileName-X.X.X-Vanilla/
|
|
467
|
+
├── mods/
|
|
468
|
+
├── saves/
|
|
469
|
+
├── config/
|
|
470
|
+
└── ...
|
|
471
|
+
|
|
472
|
+
⚙️ Java auto-installed.
|
|
473
|
+
🔧 Fabric auto-installed."""
|
|
474
|
+
ctk.CTkLabel(sidebar, text=info_text, font=("Arial", 11), text_color="#888", justify="left", wraplength=220).pack(padx=20, pady=10)
|
|
475
|
+
|
|
476
|
+
# Main content
|
|
477
|
+
main = ctk.CTkFrame(app, fg_color="transparent")
|
|
478
|
+
main.grid(row=0, column=1, sticky="nsew", padx=15, pady=15)
|
|
479
|
+
main.grid_rowconfigure(1, weight=1)
|
|
480
|
+
main.grid_columnconfigure(0, weight=1)
|
|
481
|
+
|
|
482
|
+
status_card = ctk.CTkFrame(main, fg_color="#1e1e1e", corner_radius=12)
|
|
483
|
+
status_card.grid(row=0, column=0, sticky="ew", pady=(0,15))
|
|
484
|
+
status_label = ctk.CTkLabel(status_card, text="✓ Ready", font=("Arial", 14, "bold"), text_color="#4CAF50")
|
|
485
|
+
status_label.pack(pady=15)
|
|
486
|
+
|
|
487
|
+
log_frame = ctk.CTkFrame(main, fg_color="#1e1e1e", corner_radius=12)
|
|
488
|
+
log_frame.grid(row=1, column=0, sticky="nsew")
|
|
489
|
+
log_frame.grid_rowconfigure(1, weight=1)
|
|
490
|
+
log_frame.grid_columnconfigure(0, weight=1)
|
|
491
|
+
ctk.CTkLabel(log_frame, text="CONSOLE", font=("Arial", 12, "bold"), text_color="#aaa").pack(anchor="w", padx=15, pady=(10,0))
|
|
492
|
+
log_textbox = ctk.CTkTextbox(log_frame, font=("Consolas", 11), fg_color="#0d0d0d", corner_radius=8)
|
|
493
|
+
log_textbox.pack(fill="both", expand=True, padx=15, pady=(5,15))
|
|
494
|
+
|
|
495
|
+
action_frame = ctk.CTkFrame(main, fg_color="transparent")
|
|
496
|
+
action_frame.grid(row=2, column=0, sticky="ew", pady=(15,0))
|
|
497
|
+
action_frame.grid_columnconfigure((0,1,2,3), weight=1)
|
|
498
|
+
|
|
499
|
+
ctk.CTkButton(action_frame, text="▶ PLAY NOW", command=lambda: threading.Thread(target=launch_game, daemon=True).start(),
|
|
500
|
+
fg_color="#2196F3", font=("Arial", 16, "bold"), height=50, corner_radius=12).grid(row=0, column=0, columnspan=4, sticky="ew", padx=5, pady=5)
|
|
501
|
+
|
|
502
|
+
def open_mods():
|
|
503
|
+
sel = profile_combobox.get()
|
|
504
|
+
if sel and sel in profiles:
|
|
505
|
+
p = profiles[sel]
|
|
506
|
+
loader_str = "Fabric" if p.get("loader") == "fabric" else "Vanilla"
|
|
507
|
+
inst = f"{sel}-{p.get('version', '1.21.1')}-{loader_str}".replace(" ", "_")
|
|
508
|
+
mods_path = os.path.join(VERSIONS_DIR, inst, "mods")
|
|
509
|
+
else:
|
|
510
|
+
mods_path = os.path.join(VERSIONS_DIR, "default", "mods")
|
|
511
|
+
os.makedirs(mods_path, exist_ok=True)
|
|
512
|
+
if os.name == 'nt':
|
|
513
|
+
os.startfile(mods_path)
|
|
514
|
+
else:
|
|
515
|
+
subprocess.Popen(['xdg-open', mods_path])
|
|
516
|
+
|
|
517
|
+
ctk.CTkButton(action_frame, text="📁 Mods", command=open_mods, fg_color="#4CAF50", height=40).grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
|
518
|
+
ctk.CTkButton(action_frame, text="📂 Files", command=lambda: subprocess.Popen(['xdg-open', VERSIONS_DIR] if os.name != 'nt' else ['start', VERSIONS_DIR], shell=True),
|
|
519
|
+
fg_color="#FF9800", height=40).grid(row=1, column=1, sticky="ew", padx=5, pady=5)
|
|
520
|
+
ctk.CTkButton(action_frame, text="🔧 Repair", command=lambda: repair_libraries(VERSIONS_DIR), fg_color="#F44336", height=40).grid(row=1, column=2, sticky="ew", padx=5, pady=5)
|
|
521
|
+
ctk.CTkButton(action_frame, text="🗑️ Clear", command=lambda: log_textbox.delete("1.0", "end"), fg_color="#555", height=40).grid(row=1, column=3, sticky="ew", padx=5, pady=5)
|
|
522
|
+
|
|
523
|
+
app.after(500, lambda: update_log(f"✓ Ready – {len(profiles)} profile(s)"))
|
|
524
|
+
app.mainloop()
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
load_configs()
|
|
528
|
+
build_ui()
|
|
529
|
+
|
|
530
|
+
def run():
|
|
531
|
+
"""Main entry point for the launcher"""
|
|
532
|
+
load_configs()
|
|
533
|
+
build_ui()
|
|
534
|
+
|
|
535
|
+
# Keep the existing main block
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
load_configs()
|
|
538
|
+
build_ui()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandeshlauncher
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Minecraft Launcher with auto Java, Fabric support, and profile management
|
|
5
|
+
Author: Sandesh Bhandari
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: customtkinter>=5.2.0
|
|
13
|
+
Requires-Dist: minecraft-launcher-lib>=1.11.0
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
setup.py
|
|
3
|
+
sandeshlauncher/__init__.py
|
|
4
|
+
sandeshlauncher/launcher.py
|
|
5
|
+
sandeshlauncher.egg-info/PKG-INFO
|
|
6
|
+
sandeshlauncher.egg-info/SOURCES.txt
|
|
7
|
+
sandeshlauncher.egg-info/dependency_links.txt
|
|
8
|
+
sandeshlauncher.egg-info/entry_points.txt
|
|
9
|
+
sandeshlauncher.egg-info/requires.txt
|
|
10
|
+
sandeshlauncher.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sandeshlauncher
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="sandeshlauncher",
|
|
5
|
+
version="1.0.0",
|
|
6
|
+
author="Sandesh Bhandari",
|
|
7
|
+
description="Minecraft Launcher with auto Java, Fabric support, and profile management",
|
|
8
|
+
long_description="A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.",
|
|
9
|
+
long_description_content_type="text/markdown",
|
|
10
|
+
packages=find_packages(),
|
|
11
|
+
install_requires=[
|
|
12
|
+
"customtkinter>=5.2.0",
|
|
13
|
+
"minecraft-launcher-lib>=1.11.0",
|
|
14
|
+
],
|
|
15
|
+
python_requires=">=3.7",
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
],
|
|
21
|
+
entry_points={
|
|
22
|
+
"console_scripts": [
|
|
23
|
+
"sandeshlauncher=sandeshlauncher.launcher:run",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
)
|