From: Erik Letson Date: Thu, 9 Apr 2026 00:18:37 +0000 (+0000) Subject: recreated repo X-Git-Url: https://git.mortagarden.xyz/?a=commitdiff_plain;h=cb27d66839b3ef0593680eb660ccb18c9259e7ad;p=opet.git recreated repo --- cb27d66839b3ef0593680eb660ccb18c9259e7ad diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97d4079 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +build +*.zip +*.swp + +# For those who use PyCharm +.idea/ + +# For venv +env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5eeba1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright (c) 2020 Erik Letson, all rights reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..0ce225a --- /dev/null +++ b/README @@ -0,0 +1,13 @@ +NOTE: These instructions are a WIP + +== Install from Source == +Step 1: + 'python3 -m venv env' +Step 2: + 'source env/bin/activate' +Step 3: + 'pip3 install -r requirements.txt' +Step 4: + Install wxPython for your particular Linux distro. Refer to the Linux wheels section here: + https://wxpython.org/pages/downloads/ + NOTE: These instructions will be improved in the future. diff --git a/environment.json b/environment.json new file mode 100644 index 0000000..cbae5b5 --- /dev/null +++ b/environment.json @@ -0,0 +1,8 @@ +{ + "LaunchSetup" : false, + "OblivionPath" : "~/.steam/steamapps/common/Oblivion", + "OblivionDataPath" : "~/.steam/steamapps/common/Oblivion/Data", + "OblivionPrefixPath" : "~/.steam/steamapps/compatdata/22330/pfx", + "OblivionPrefixAppdata" : "~/.steam/steamapps/compatdata/22330/pfx/drive_c/users/steamuser/Local Settings/Application Data/Oblivion", + "StoragePath" : "~/.local/share/opet" +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..f818718 --- /dev/null +++ b/main.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python + +from src import opet + +def main(): + o = opet.OPET() + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..44c94b3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy==1.19.4 +Pillow==8.1.0 +six==1.15.0 diff --git a/src/datas.py b/src/datas.py new file mode 100644 index 0000000..87bef7b --- /dev/null +++ b/src/datas.py @@ -0,0 +1,36 @@ +############ +# datas.py # +############ + +# This file contains: +# 1. Various data structures used in OPET + +###################################### +# Section 1 - Custom data structures # +###################################### + +class OrderList(object): + """ + Data structure representing an organized list + of esp files along with their activation status, + and comments about them. + """ + + def __init__(self, manager, order, enabled, comments = {}): + + # Setup + self.manager = manager + self.init_order = order + self.init_enabled = enabled + self.init_comments = comments + + self.data = [] + ind = 0 + for e in order: + self.data.append({ "name" : e, "order" : ind, "enabled" : True if e in enabled else False, "comments" : comments[e] if e in comments.keys() else "", "just_changed" : False }) + ind += 1 + + def get_data(self): + return self.data + def set_data(self, data): + self.data = data diff --git a/src/filer.py b/src/filer.py new file mode 100644 index 0000000..cad16a9 --- /dev/null +++ b/src/filer.py @@ -0,0 +1,42 @@ +import os, json + +############ +# filer.py # +############ + +# This file contains: +# 1. The Filer class, which manages any file i/o operations + +########################### +# Section 1 - Filer class # +########################### + +class Filer(object): + """ + The Filer object manages reading from and + writing to the various files that OPET uses + and/or edits. + """ + def __init__(self, manager, environment_file): + self.manager = manager + self.environment_file = environment_file + self.environment = self.read_json(self.environment_file) + + def get_environment(self): + return self.environment + + def read_raw_file(self, filename): + with open(filename, "r") as f: + return f.readlines() + + def read_json(self, jsonfile): + with open(jsonfile, "r") as j: + return json.load(j) + + def write_raw_file(self, filename, string): + with open(filename, "w") as f: + f.write(string) + + def write_json(self, jsonfile, jsondata): + with open(filename, "w") as j: + json.dump(jsondata, j) diff --git a/src/opet.py b/src/opet.py new file mode 100644 index 0000000..ee8269d --- /dev/null +++ b/src/opet.py @@ -0,0 +1,223 @@ +import wx, os +from . import filer, tools, wxcustom, datas + +########### +# opet.py # +########### + +# This file contains: +# 1. The OPET class, which is the main application object +# 2. The PrimeFrame class, which represents the dominant OPET frame and is managed by OPET + +########################## +# Section 1 - OPET class # +########################## + +class OPET(object): + """ + OPET is the main application object. It + manages the other constituent objects + and controls the application's entire + operation. + """ + def __init__(self): + + # Various tools + self.filer = filer.Filer(self, "environment.json") + self.environment = self.filer.get_environment() + self.esp_tool = tools.DataPackTool(self) + + # Data + rel = self.filer.read_raw_file(os.path.join(os.path.expanduser(self.environment["OblivionPrefixAppdata"]), "Plugins.txt")) + enabled_list = [] + for l in rel: + if l[0] not in ['#', '\n']: + if l[len(l) - 1] == '\n': + l = l[0:len(l) - 1] + enabled_list.append(l) + self.esp_data = datas.OrderList(self, self.esp_tool.get_load_order_list(os.path.expanduser(self.environment["OblivionDataPath"])), enabled_list) + + # Load up app and frame components + self.app = wx.App() + self.app_frame = OPETFrame(self, None, title = "OPET", size = (1200, 800)) + self.app_frame.Show() + self.app.MainLoop() + +################################ +# Section 2 - PrimeFrame class # +################################ + +class OPETFrame(wx.Frame): + """ + The wxWidget frame representing the + application's main window. + """ + + def __init__(self, manager, *args, **kwargs): + + # Parent initialization + super(OPETFrame, self).__init__(*args, **kwargs) + + # Important vals + self.manager = manager + + # Frame geometry + self.main_panel = wx.Panel(self) + self.main_sizer = wx.BoxSizer(wx.VERTICAL) + self.main_panel.SetSizer(self.main_sizer) + + # Construct menu bar + # .. File menu + self.file_menu = wx.Menu() + self.new_profile_item = self.file_menu.Append(-1, "&New Profile...\tCtrl-N", "Create a new empty mod list profile") + self.Bind(wx.EVT_MENU, self.on_new_profile, self.new_profile_item) + self.open_profile_item = self.file_menu.Append(-1, "&Open Profile...\tCtrl-O", "Open an existing mod list profile") + self.Bind(wx.EVT_MENU, self.on_open_profile, self.open_profile_item) + self.close_profile_item = self.file_menu.Append(-1, "Close Profile...\tCtrl-Q", "Close current mod list profile") + self.Bind(wx.EVT_MENU, self.on_close_profile, self.close_profile_item) + self.file_menu.AppendSeparator() + self.save_profile_item = self.file_menu.Append(-1, "&Save Profile...\tCtrl-S", "Save current mod list") + self.Bind(wx.EVT_MENU, self.on_save_profile, self.save_profile_item) + self.save_profile_as_item = self.file_menu.Append(-1, "&Save Profile As...\tCtrl-Shift-S", "Save current mod list as...") + self.Bind(wx.EVT_MENU, self.on_save_profile_as, self.save_profile_as_item) + self.file_menu.AppendSeparator() + self.exit_item = self.file_menu.Append(wx.ID_EXIT) + self.Bind(wx.EVT_MENU, self.on_exit, self.exit_item) + # .. Edit menu + self.edit_menu = wx.Menu() + # .. Tools menu + self.tools_menu = wx.Menu() + # .. Help menu + self.help_menu = wx.Menu() + self.about_item = self.help_menu.Append(wx.ID_ABOUT) + self.Bind(wx.EVT_MENU, self.on_about, self.about_item) + # Menu bar itself + self.menu_bar = wx.MenuBar() + self.menu_bar.Append(self.file_menu, "&File") + self.menu_bar.Append(self.edit_menu, "&Edit") + self.menu_bar.Append(self.tools_menu, "&Tools") + self.menu_bar.Append(self.help_menu, "&Help") + self.SetMenuBar(self.menu_bar) + + # Construct sub-panel + self.sub_panel = wx.Panel(self.main_panel) + self.sub_panel_sizer = wx.BoxSizer(wx.VERTICAL) + self.sub_panel.SetSizer(self.sub_panel_sizer) + self.sub_panel.SetSize(4, 4, 1200, 800) + + # Construct esp list + self.esp_list_panel = wx.Panel(self.sub_panel) + self.esp_list_sizer = wx.BoxSizer(wx.VERTICAL) + self.esp_list_panel.SetSizer(self.esp_list_sizer) + self.esp_list_ctrl = wxcustom.ESPListCtrl(self.esp_list_panel, size = (800, 500)) + self.esp_list_ctrl.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self.OnBeginLabelEdit) + self.esp_list_ctrl.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.OnEndLabelEdit) + self.update_esp_list(self.manager.esp_data.get_data()) + self.esp_buttons_panel = wx.Panel(self.sub_panel) + self.esp_buttons_panel_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.esp_buttons_panel.SetSizer(self.esp_buttons_panel_sizer) + self.esp_apply_button = wxcustom.ESPApplyButton(self.esp_buttons_panel) + self.esp_apply_button.Bind(wx.EVT_BUTTON, self.OnApplyChanges) + self.esp_list_panel.SetSize(8, 8, 800, 1100) + self.esp_buttons_panel.SetSize(8, 600, 800, 100) + + def update_esp_list(self, data): + self.esp_list_ctrl.DeleteAllItems() + for e in data: + entry = ["", e["order"], e["name"], e["comments"]] + self.esp_list_ctrl.Append(entry) + if e["enabled"]: + self.esp_list_ctrl.CheckItem(int(e["order"]), True) + self.manager.esp_data.set_data(data) + + def sort_esp_data_by_order(self, data): + # TODO: This is terrible and inefficient + simsor = sorted(data, key=lambda k: k["order"]) + j = None + for i in simsor: + if i["just_changed"]: + if j["order"] == i["order"]: + j["order"] += 1 + elif j != None and j["just_changed"]: + if i["order"] == j["order"]: + i["order"] -= 1 + j = i + simsor = sorted(data, key=lambda k: k["order"]) + oldor = -1 + for i in simsor: + if i["order"] != oldor + 1: + i["order"] = oldor + 1 + oldor = i["order"] + return simsor + + # TODO: Cross-object binding issues indicate larger program construction problems??? + def OnApplyChanges(self, event): + data = [] + for row in range(self.esp_list_ctrl.GetItemCount()): + n = self.esp_list_ctrl.GetItem(row, col = 2).GetText() + o = int(self.esp_list_ctrl.GetItem(row, col = 1).GetText()) + e = self.esp_list_ctrl.IsItemChecked(row) + c = self.esp_list_ctrl.GetItem(row, col = 3).GetText() + dd = { "name" : n, "order" : o, "enabled" : e, "comments" : c, "just_changed" : False } + data.append(dd) + self.update_esp_list(self.sort_esp_data_by_order(data)) + self.manager.esp_tool.assign_load_order(self.manager.esp_data.get_data(), self.manager.environment) + self.manager.esp_tool.enable_esps(self.manager.esp_data.get_data(), self.manager.environment) + def OnBeginLabelEdit(self, event): + col = event.GetColumn() + fi = self.esp_list_ctrl.GetFocusedItem() + if col == 0: + event.Veto() + if fi != -1: + self.esp_list_ctrl.CheckItem(fi, not self.esp_list_ctrl.IsItemChecked(fi)) + elif col == 1: + if fi != -1: + self.esp_list_ctrl.last_order_value = int(self.esp_list_ctrl.GetItem(fi, 1).GetText()) + event.Skip() + elif col == 2: + event.Veto() + else: + event.Skip() + def OnEndLabelEdit(self, event): + if event.GetColumn() == 1: + try: + lbint = int(event.GetLabel()) + except ValueError: + lbint = int(self.esp_list_ctrl.GetItem(event.GetItem(), event.GetColumn())) + if lbint < 0: + lbint = 0 + if self.esp_list_ctrl.last_order_value != None and lbint != self.esp_list_ctrl.last_order_value: + self.esp_list_ctrl.SetItem(event.GetIndex(), event.GetColumn(), str(lbint)) + # TODO: This is a bit fragile and relies on file names... + entry = self.esp_list_ctrl.GetItem(event.GetIndex(), col = 2).GetText() + data = [] + for row in range(self.esp_list_ctrl.GetItemCount()): + n = self.esp_list_ctrl.GetItem(row, col = 2).GetText() + o = int(self.esp_list_ctrl.GetItem(row, col = 1).GetText()) + e = self.esp_list_ctrl.IsItemChecked(row) + c = self.esp_list_ctrl.GetItem(row, col = 3).GetText() + j = True if n == entry else False + dd = { "name" : n, "order" : o, "enabled" : e, "comments" : c, "just_changed" : j } + data.append(dd) + self.update_esp_list(self.sort_esp_data_by_order(data)) + for i in self.manager.esp_data.get_data(): + if i["just_changed"]: + #self.esp_list_ctrl.EnsureVisible(int(i["order"])) + self.esp_list_ctrl.Focus(int(i["order"])) + break + event.Veto() + + def on_new_profile(self, event): + pass + def on_open_profile(self, event): + pass + def on_close_profile(self, event): + pass + def on_save_profile(self, event): + pass + def on_save_profile_as(self, event): + pass + def on_exit(self, event): + self.Close(True) + def on_about(self, event): + wx.MessageBox("Test", "Test test", wx.OK|wx.ICON_INFORMATION) diff --git a/src/tools.py b/src/tools.py new file mode 100644 index 0000000..cae124a --- /dev/null +++ b/src/tools.py @@ -0,0 +1,55 @@ +import os, sys, time + +############ +# tools.py # +############ + +# This file contains: +# 1. Various objects that represent certain functionality of OPET + +##################### +# Section 1 - Tools # +##################### + +class DataPackTool(object): + """ + Encapsulates the functionality of OPET + to set .es[p|m] load order and activation. + """ + + def __init__(self, manager): + + # Setup + self.manager = manager + + def get_load_order_list(self, path): + """ + Construct a load order list out of the + .es[p|m] files in the given directory and + return it. + """ + matches = [] + for f in list(sorted(os.listdir(path), key = lambda x: os.stat(os.path.join(path, x)).st_mtime)): + if os.path.splitext(f)[1] in [".esp", ".esm"]: + matches.append(f) + return matches + + def assign_load_order(self, orderlist, environment): + """ + Assign load order by modyfing date + of files in order of their appearance + in 'orderlist'. + """ + for e in orderlist: + os.utime(os.path.join(os.path.expanduser(environment["OblivionDataPath"]), e["name"])) + time.sleep(0.01) + + def enable_esps(self, enablelist, environment): + """ + Enable the given ESPs. + """ + with open(os.path.join(os.path.expanduser(environment["OblivionPrefixAppdata"]), "Plugins.txt"), "w") as f: + for e in enablelist: + if e["enabled"]: + f.write(e["name"] + '\n') + diff --git a/src/wxcustom.py b/src/wxcustom.py new file mode 100644 index 0000000..32eabbd --- /dev/null +++ b/src/wxcustom.py @@ -0,0 +1,54 @@ +import wx +from wx.lib.mixins import listctrl + +############### +# wxcustom.py # +############### + +# This file contains: +# 1. Various custom classes derived from wx elements + +################################# +# Section 1 - Custom wx classes # +################################# + +class ESPListCtrl(wx.ListCtrl, listctrl.TextEditMixin, listctrl.ListCtrlAutoWidthMixin): + """ + ListCtrl object that is used to edit + the list of esp files that should be + enabled and also their load order. + """ + + def __init__(self, parent, size = (400, 300)): + + # Parent initialization + wx.ListCtrl.__init__(self, parent, size = size, style = wx.LC_REPORT) + listctrl.TextEditMixin.__init__(self) + listctrl.ListCtrlAutoWidthMixin.__init__(self) + + # Set values + self.setResizeColumn(3) + self.EnableCheckBoxes(True) + + # Dynamics + # TODO: Maybe this should be in the parent object instead??? + # Depends on how to solve the problem of where to bind... + self.last_order_value = None + + # Columns + self.InsertColumn(0, "Enabled") + self.InsertColumn(1, "Order") + self.InsertColumn(2, "File") + self.InsertColumn(3, "Comment") + +class ESPApplyButton(wx.Button): + """ + The ESPApplyButton is used to apply + any changes made to the esp list + to the actual file system. + """ + + def __init__(self, parent, label = "Apply"): + + # Parent initialization + wx.Button.__init__(self, parent, label = label) diff --git a/util/dumpesp.sh b/util/dumpesp.sh new file mode 100755 index 0000000..f373fde --- /dev/null +++ b/util/dumpesp.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +INSTALLPATH=$HOME/.steam/steamapps/common/Oblivion/Data +OUTFILE=espfile.txt + +ls -tr $INSTALLPATH | grep '.*es[p|m]$' > $OUTFILE diff --git a/util/orderesp.py b/util/orderesp.py new file mode 100644 index 0000000..1bf2a74 --- /dev/null +++ b/util/orderesp.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import os, sys, time + +HOME = os.path.expanduser("~") +TOUCHPATH = os.path.join(HOME, ".steam", "steamapps", "common", "Oblivion", "Data") + +def touch_by_file(filename): + with open(filename, "r") as touchlist: + for line in touchlist.readlines(): + line = line.replace(" ", "\\ ") + line = line.replace("&", "\\&") + line = line.replace("\'", "\\\'") + line = line.replace("[", "\\[") + line = line.replace("]", "\\]") + os.system("touch -c " + TOUCHPATH + "/" + line) + time.sleep(0.1) + +if __name__ == "__main__": + touch_by_file(sys.argv[1]) diff --git a/util/togglelauncher.sh b/util/togglelauncher.sh new file mode 100755 index 0000000..775d375 --- /dev/null +++ b/util/togglelauncher.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +INSTALLPATH=$HOME/.steam/steamapps/common/Oblivion +LAUNCHEREXE=OblivionLauncher.exe +OFFEXENAME=TOGGLEDOFF.exe + +mv -i $INSTALLPATH/$LAUNCHEREXE tmp.exe +mv -i $INSTALLPATH/$OFFEXENAME $INSTALLPATH/$LAUNCHEREXE +mv -i tmp.exe $INSTALLPATH/$OFFEXENAME