]> git.mortagarden.xyz Git - opet.git/commitdiff
recreated repo
authorErik Letson <erik@mortagarden.xyz>
Thu, 9 Apr 2026 00:18:37 +0000 (00:18 +0000)
committerErik Letson <erik@mortagarden.xyz>
Thu, 9 Apr 2026 00:18:37 +0000 (00:18 +0000)
14 files changed:
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README [new file with mode: 0644]
environment.json [new file with mode: 0644]
main.py [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
src/datas.py [new file with mode: 0644]
src/filer.py [new file with mode: 0644]
src/opet.py [new file with mode: 0644]
src/tools.py [new file with mode: 0644]
src/wxcustom.py [new file with mode: 0644]
util/dumpesp.sh [new file with mode: 0755]
util/orderesp.py [new file with mode: 0644]
util/togglelauncher.sh [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..97d4079
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..cbae5b5
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..44c94b3
--- /dev/null
@@ -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 (file)
index 0000000..87bef7b
--- /dev/null
@@ -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 (file)
index 0000000..cad16a9
--- /dev/null
@@ -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 (file)
index 0000000..ee8269d
--- /dev/null
@@ -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 (file)
index 0000000..cae124a
--- /dev/null
@@ -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 (file)
index 0000000..32eabbd
--- /dev/null
@@ -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 (executable)
index 0000000..f373fde
--- /dev/null
@@ -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 (file)
index 0000000..1bf2a74
--- /dev/null
@@ -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 (executable)
index 0000000..775d375
--- /dev/null
@@ -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