Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

27 changed files with 6765 additions and 44 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

45
.gitignore vendored
View File

@ -1,8 +1,7 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
@ -30,6 +29,7 @@ x86/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
@ -83,8 +83,6 @@ StyleCopReport.xml
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
@ -93,7 +91,6 @@ StyleCopReport.xml
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
@ -297,17 +294,6 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@ -364,9 +350,6 @@ ASALocalRun/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
@ -377,26 +360,4 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
FodyWeavers.xsd

13
App.config Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<!-- Empty = %AppData%\D2Multi\accounts.xml (see D2AccountsXmlStore.DefaultPath) -->
<add key="AccountsXmlPath" value="" />
<add key="D2RExePath" value="E:\Games2\Diablo II Resurrected\D2R.exe" />
<!-- Optional: full path to Sysinternals handle64.exe. Empty = D2R folder\handle64.exe -->
<add key="Handle64ExePath" value="" />
</appSettings>
</configuration>

180
CharacterMissionGridRow.cs Normal file
View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace D2Multi
{
/// <summary>Grid row binding for <see cref="D2Character"/> mission flags across difficulties.</summary>
public sealed class CharacterMissionGridRow : INotifyPropertyChanged
{
readonly D2Character _character;
public CharacterMissionGridRow(D2Character character)
{
_character = character ?? throw new ArgumentNullException(nameof(character));
D2CharacterMissionHelper.EnsureAllMissions(_character);
}
public D2Character Character => _character;
public string Name
{
get => _character.Name ?? string.Empty;
set
{
var v = value ?? string.Empty;
if (string.Equals(_character.Name, v, StringComparison.Ordinal))
return;
_character.Name = v;
OnPropertyChanged(nameof(Name));
}
}
public string Class
{
get => _character.Class ?? string.Empty;
set
{
var v = value ?? string.Empty;
if (string.Equals(_character.Class, v, StringComparison.Ordinal))
return;
_character.Class = v;
OnPropertyChanged(nameof(Class));
}
}
public bool NormalA1
{
get => Mission(D2Level.Normal).A1_Enpowered;
set => SetA1(D2Level.Normal, value);
}
public bool NormalA5
{
get => Mission(D2Level.Normal).A5_Socket;
set => SetA5(D2Level.Normal, value);
}
public bool NightmareA1
{
get => Mission(D2Level.Nightmare).A1_Enpowered;
set => SetA1(D2Level.Nightmare, value);
}
public bool NightmareA5
{
get => Mission(D2Level.Nightmare).A5_Socket;
set => SetA5(D2Level.Nightmare, value);
}
public bool HellA1
{
get => Mission(D2Level.Hell).A1_Enpowered;
set => SetA1(D2Level.Hell, value);
}
public bool HellA5
{
get => Mission(D2Level.Hell).A5_Socket;
set => SetA5(D2Level.Hell, value);
}
/// <summary>Count of quest rewards not yet claimed (false = still available).</summary>
public int MissionsAvailable
{
get
{
var n = 0;
foreach (var lvl in AllLevels)
{
var m = Mission(lvl);
if (!m.A1_Enpowered) n++;
if (!m.A5_Socket) n++;
}
return n;
}
}
static readonly D2Level[] AllLevels = { D2Level.Normal, D2Level.Nightmare, D2Level.Hell };
D2Mission Mission(D2Level level) => D2CharacterMissionHelper.GetMission(_character, level);
void SetA1(D2Level level, bool value)
{
var m = Mission(level);
if (m.A1_Enpowered == value) return;
m.A1_Enpowered = value;
OnPropertyChanged(PropForA1(level));
OnPropertyChanged(nameof(MissionsAvailable));
}
void SetA5(D2Level level, bool value)
{
var m = Mission(level);
if (m.A5_Socket == value) return;
m.A5_Socket = value;
OnPropertyChanged(PropForA5(level));
OnPropertyChanged(nameof(MissionsAvailable));
}
static string PropForA1(D2Level level)
{
switch (level)
{
case D2Level.Normal: return nameof(NormalA1);
case D2Level.Nightmare: return nameof(NightmareA1);
default: return nameof(HellA1);
}
}
static string PropForA5(D2Level level)
{
switch (level)
{
case D2Level.Normal: return nameof(NormalA5);
case D2Level.Nightmare: return nameof(NightmareA5);
default: return nameof(HellA5);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public static List<CharacterMissionGridRow> FromAccount(D2Account account)
{
if (account?.Characters == null || account.Characters.Count == 0)
return new List<CharacterMissionGridRow>();
return account.Characters.Select(c => new CharacterMissionGridRow(c)).ToList();
}
}
static class D2CharacterMissionHelper
{
public static void EnsureAllMissions(D2Character character)
{
if (character.Missions == null)
character.Missions = new List<D2Mission>();
foreach (D2Level lvl in Enum.GetValues(typeof(D2Level)))
{
if (character.Missions.All(m => m.Level != lvl))
character.Missions.Add(new D2Mission { Level = lvl });
}
}
public static D2Mission GetMission(D2Character character, D2Level level)
{
EnsureAllMissions(character);
return character.Missions.First(m => m.Level == level);
}
public static D2Character CreateNewCharacter()
{
var c = new D2Character { Name = "New character", Class = string.Empty, Missions = new List<D2Mission>() };
EnsureAllMissions(c);
return c;
}
}
}

164
CharacterMissionsForm.Designer.cs generated Normal file
View File

@ -0,0 +1,164 @@
namespace D2Multi
{
partial class CharacterMissionsForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
#region Windows Form Designer generated code
void InitializeComponent()
{
this.dgvCharacters = new System.Windows.Forms.DataGridView();
this.btnAddCharacter = new System.Windows.Forms.Button();
this.btnRemoveCharacter = new System.Windows.Forms.Button();
this.btnOk = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.panelBottom = new System.Windows.Forms.TableLayoutPanel();
this.flowLeftButtons = new System.Windows.Forms.FlowLayoutPanel();
this.flowRightButtons = new System.Windows.Forms.FlowLayoutPanel();
((System.ComponentModel.ISupportInitialize)(this.dgvCharacters)).BeginInit();
this.panelBottom.SuspendLayout();
this.flowLeftButtons.SuspendLayout();
this.flowRightButtons.SuspendLayout();
this.SuspendLayout();
//
// dgvCharacters
//
this.dgvCharacters.AllowUserToAddRows = false;
this.dgvCharacters.AllowUserToDeleteRows = false;
this.dgvCharacters.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvCharacters.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvCharacters.Location = new System.Drawing.Point(0, 0);
this.dgvCharacters.Name = "dgvCharacters";
this.dgvCharacters.RowHeadersWidth = 40;
this.dgvCharacters.RowTemplate.Height = 24;
this.dgvCharacters.Size = new System.Drawing.Size(1183, 401);
this.dgvCharacters.TabIndex = 0;
//
// btnAddCharacter
//
this.btnAddCharacter.AutoSize = true;
this.btnAddCharacter.Location = new System.Drawing.Point(3, 3);
this.btnAddCharacter.Name = "btnAddCharacter";
this.btnAddCharacter.Size = new System.Drawing.Size(120, 28);
this.btnAddCharacter.TabIndex = 1;
this.btnAddCharacter.Text = "Add character";
this.btnAddCharacter.UseVisualStyleBackColor = true;
this.btnAddCharacter.Click += new System.EventHandler(this.BtnAddCharacter_Click);
//
// btnRemoveCharacter
//
this.btnRemoveCharacter.AutoSize = true;
this.btnRemoveCharacter.Location = new System.Drawing.Point(129, 3);
this.btnRemoveCharacter.Name = "btnRemoveCharacter";
this.btnRemoveCharacter.Size = new System.Drawing.Size(140, 28);
this.btnRemoveCharacter.TabIndex = 2;
this.btnRemoveCharacter.Text = "Remove character";
this.btnRemoveCharacter.UseVisualStyleBackColor = true;
this.btnRemoveCharacter.Click += new System.EventHandler(this.BtnRemoveCharacter_Click);
//
// btnOk
//
this.btnOk.DialogResult = System.Windows.Forms.DialogResult.OK;
this.btnOk.Location = new System.Drawing.Point(505, 3);
this.btnOk.Name = "btnOk";
this.btnOk.Size = new System.Drawing.Size(75, 28);
this.btnOk.TabIndex = 3;
this.btnOk.Text = "OK";
this.btnOk.UseVisualStyleBackColor = true;
//
// btnCancel
//
this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.btnCancel.Location = new System.Drawing.Point(424, 3);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(75, 28);
this.btnCancel.TabIndex = 4;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = true;
//
// panelBottom
//
this.panelBottom.ColumnCount = 2;
this.panelBottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.panelBottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.panelBottom.Controls.Add(this.flowLeftButtons, 0, 0);
this.panelBottom.Controls.Add(this.flowRightButtons, 1, 0);
this.panelBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
this.panelBottom.Location = new System.Drawing.Point(0, 401);
this.panelBottom.Name = "panelBottom";
this.panelBottom.RowCount = 1;
this.panelBottom.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.panelBottom.Size = new System.Drawing.Size(1183, 40);
this.panelBottom.TabIndex = 5;
//
// flowLeftButtons
//
this.flowLeftButtons.AutoSize = true;
this.flowLeftButtons.Controls.Add(this.btnAddCharacter);
this.flowLeftButtons.Controls.Add(this.btnRemoveCharacter);
this.flowLeftButtons.Dock = System.Windows.Forms.DockStyle.Fill;
this.flowLeftButtons.Location = new System.Drawing.Point(3, 3);
this.flowLeftButtons.Name = "flowLeftButtons";
this.flowLeftButtons.Size = new System.Drawing.Size(585, 34);
this.flowLeftButtons.TabIndex = 0;
this.flowLeftButtons.WrapContents = false;
//
// flowRightButtons
//
this.flowRightButtons.AutoSize = true;
this.flowRightButtons.Controls.Add(this.btnOk);
this.flowRightButtons.Controls.Add(this.btnCancel);
this.flowRightButtons.Dock = System.Windows.Forms.DockStyle.Fill;
this.flowRightButtons.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft;
this.flowRightButtons.Location = new System.Drawing.Point(594, 3);
this.flowRightButtons.Name = "flowRightButtons";
this.flowRightButtons.Padding = new System.Windows.Forms.Padding(0, 0, 3, 0);
this.flowRightButtons.Size = new System.Drawing.Size(586, 34);
this.flowRightButtons.TabIndex = 1;
this.flowRightButtons.WrapContents = false;
//
// CharacterMissionsForm
//
this.AcceptButton = this.btnOk;
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnCancel;
this.ClientSize = new System.Drawing.Size(1183, 441);
this.Controls.Add(this.dgvCharacters);
this.Controls.Add(this.panelBottom);
this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(700, 400);
this.Name = "CharacterMissionsForm";
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Characters";
((System.ComponentModel.ISupportInitialize)(this.dgvCharacters)).EndInit();
this.panelBottom.ResumeLayout(false);
this.panelBottom.PerformLayout();
this.flowLeftButtons.ResumeLayout(false);
this.flowLeftButtons.PerformLayout();
this.flowRightButtons.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.DataGridView dgvCharacters;
private System.Windows.Forms.Button btnAddCharacter;
private System.Windows.Forms.Button btnRemoveCharacter;
private System.Windows.Forms.Button btnOk;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.TableLayoutPanel panelBottom;
private System.Windows.Forms.FlowLayoutPanel flowLeftButtons;
private System.Windows.Forms.FlowLayoutPanel flowRightButtons;
}
}

141
CharacterMissionsForm.cs Normal file
View File

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
namespace D2Multi
{
public partial class CharacterMissionsForm : Form
{
readonly D2Account _account;
readonly List<D2Character> _editingCharacters;
BindingList<CharacterMissionGridRow> _rows;
BindingSource _source;
public CharacterMissionsForm(D2Account account, string accountLabel)
{
if (account == null)
throw new ArgumentNullException(nameof(account));
_account = account;
_editingCharacters = CloneCharacterList(account.Characters);
InitializeComponent();
Text = "Characters — " + (accountLabel ?? account.Name ?? "Account");
BuildGrid();
}
static List<D2Character> CloneCharacterList(IEnumerable<D2Character> src)
{
var list = new List<D2Character>();
if (src == null)
return list;
foreach (var c in src)
{
if (c == null)
continue;
list.Add(CloneCharacter(c));
}
return list;
}
static D2Character CloneCharacter(D2Character c)
{
var missions = c.Missions == null
? new List<D2Mission>()
: c.Missions.Where(m => m != null).Select(m => new D2Mission
{
Level = m.Level,
A1_Enpowered = m.A1_Enpowered,
A5_Socket = m.A5_Socket,
}).ToList();
return new D2Character { Name = c.Name, Class = c.Class, Missions = missions };
}
void BuildGrid()
{
var wrapAccount = new D2Account { Characters = _editingCharacters };
_rows = new BindingList<CharacterMissionGridRow>(CharacterMissionGridRow.FromAccount(wrapAccount));
_source = new BindingSource { DataSource = _rows };
dgvCharacters.AutoGenerateColumns = false;
dgvCharacters.Columns.Clear();
void AddText(string prop, string header, int width)
{
dgvCharacters.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = prop,
HeaderText = header,
Width = width,
});
}
void AddBool(string prop, string header, string tip, int width)
{
var col = new DataGridViewCheckBoxColumn
{
DataPropertyName = prop,
HeaderText = header,
Width = width,
ThreeState = false,
};
col.ToolTipText = tip;
dgvCharacters.Columns.Add(col);
}
AddText(nameof(CharacterMissionGridRow.Name), "Character", 140);
AddText(nameof(CharacterMissionGridRow.Class), "Class", 100);
const string tipDone = "Checked = reward taken (no longer available). Unchecked = still available.";
AddBool(nameof(CharacterMissionGridRow.NormalA1), "Normal A1", tipDone, 85);
AddBool(nameof(CharacterMissionGridRow.NormalA5), "Normal A5", tipDone, 85);
AddBool(nameof(CharacterMissionGridRow.NightmareA1), "NM A1", tipDone, 70);
AddBool(nameof(CharacterMissionGridRow.NightmareA5), "NM A5", tipDone, 70);
AddBool(nameof(CharacterMissionGridRow.HellA1), "Hell A1", tipDone, 75);
AddBool(nameof(CharacterMissionGridRow.HellA5), "Hell A5", tipDone, 75);
dgvCharacters.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CharacterMissionGridRow.MissionsAvailable),
HeaderText = "Available",
ReadOnly = true,
Width = 80,
ToolTipText = "Count of rewards not yet claimed (A1 + A5 across all difficulties).",
});
dgvCharacters.DataSource = _source;
}
void BtnAddCharacter_Click(object sender, EventArgs e)
{
var c = D2CharacterMissionHelper.CreateNewCharacter();
_editingCharacters.Add(c);
_rows.Add(new CharacterMissionGridRow(c));
if (_rows.Count > 0)
dgvCharacters.CurrentCell = dgvCharacters.Rows[_rows.Count - 1].Cells[0];
}
void BtnRemoveCharacter_Click(object sender, EventArgs e)
{
if (_source.Current is CharacterMissionGridRow row)
{
_editingCharacters.Remove(row.Character);
_rows.Remove(row);
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (DialogResult == DialogResult.OK)
{
dgvCharacters.EndEdit();
_source.EndEdit();
if (_editingCharacters.Any(c => string.IsNullOrWhiteSpace(c?.Name)))
{
e.Cancel = true;
MessageBox.Show(this, "Each character needs a name.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_account.Characters = _editingCharacters;
}
base.OnFormClosing(e);
}
}
}

120
CharacterMissionsForm.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

BIN
D2.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

67
D2Account.cs Normal file
View File

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Xml.Serialization;
namespace D2Multi
{
public class D2Account : INotifyPropertyChanged
{
private string _name;
private string _email;
private string _encryptedPassword;
private string _password;
public string Name
{
get => _name;
set { if (_name == value) return; _name = value; OnPropertyChanged(nameof(Name)); }
}
public string Email
{
get => _email;
set { if (_email == value) return; _email = value; OnPropertyChanged(nameof(Email)); }
}
/// <summary>DPAPI-protected secret, Base64. Persisted to XML only.</summary>
public string EncryptedPassword
{
get => _encryptedPassword;
set { if (_encryptedPassword == value) return; _encryptedPassword = value; OnPropertyChanged(nameof(EncryptedPassword)); }
}
/// <summary>Transient plain text for editing; never written to XML.</summary>
[XmlIgnore]
public string Password
{
get => _password;
set { if (_password == value) return; _password = value; OnPropertyChanged(nameof(Password)); }
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public List<D2Character> Characters { get; set; }
}
public class D2Character
{
public string Name { get; set; }
public string Class { get; set; }
public List<D2Mission> Missions { get; set; }
}
public enum D2Level
{
Normal,
Nightmare,
Hell
}
public class D2Mission
{
public D2Level Level { get; set; }
public bool A1_Enpowered { get; set; }
public bool A5_Socket { get; set; }
}
}

36
D2AccountCrypto.cs Normal file
View File

@ -0,0 +1,36 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace D2Multi
{
static class D2AccountCrypto
{
static readonly byte[] Entropy = Encoding.UTF8.GetBytes("D2Multi.Accounts.v1");
public static string Protect(string plain)
{
if (string.IsNullOrEmpty(plain))
return string.Empty;
var bytes = Encoding.UTF8.GetBytes(plain);
var protectedBytes = ProtectedData.Protect(bytes, Entropy, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
public static string Unprotect(string base64Cipher)
{
if (string.IsNullOrEmpty(base64Cipher))
return string.Empty;
try
{
var protectedBytes = Convert.FromBase64String(base64Cipher);
var bytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(bytes);
}
catch
{
return string.Empty;
}
}
}
}

34
D2AccountPersisted.cs Normal file
View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace D2Multi
{
/// <summary>XML shape only; encryption is applied in <see cref="D2AccountsXmlStore"/> when saving.</summary>
public class D2AccountPersisted
{
public string Name { get; set; }
public string Email { get; set; }
[XmlElement("EncryptedPassword")]
public string EncryptedPassword { get; set; }
[XmlElement("Character")]
public List<D2CharacterPersisted> Characters { get; set; }
}
public class D2CharacterPersisted
{
public string Name { get; set; }
public string Class { get; set; }
[XmlElement("Mission")]
public List<D2MissionPersisted> Missions { get; set; }
}
public class D2MissionPersisted
{
public D2Level Level { get; set; }
public bool A1_Enpowered { get; set; }
public bool A5_Socket { get; set; }
}
}

144
D2AccountsXmlStore.cs Normal file
View File

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
namespace D2Multi
{
[XmlRoot("Accounts")]
public class D2AccountsRoot
{
[XmlElement("Account")]
public List<D2AccountPersisted> AccountList { get; set; } = new List<D2AccountPersisted>();
}
static class D2AccountsXmlStore
{
public static string DefaultPath =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "accounts.xml");
public static List<D2Account> Load(string path)
{
if (!File.Exists(path))
return new List<D2Account>();
using (var stream = File.OpenRead(path))
{
var ser = new XmlSerializer(typeof(D2AccountsRoot));
var root = (D2AccountsRoot)ser.Deserialize(stream);
if (root?.AccountList == null)
return new List<D2Account>();
var result = new List<D2Account>(root.AccountList.Count);
foreach (var p in root.AccountList)
{
var cipher = p?.EncryptedPassword ?? string.Empty;
result.Add(new D2Account
{
Name = p?.Name,
Email = p?.Email,
EncryptedPassword = cipher,
Password = D2AccountCrypto.Unprotect(cipher),
Characters = MapCharactersFromPersisted(p?.Characters),
});
}
return result;
}
}
/// <summary>Builds XML from accounts; encrypts only here from <see cref="D2Account.Password"/> when non-empty.</summary>
public static void Save(string path, IEnumerable<D2Account> accounts)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var persisted = (accounts ?? Enumerable.Empty<D2Account>())
.Select(ToPersistedForXml)
.ToList();
var root = new D2AccountsRoot { AccountList = persisted };
using (var stream = File.Create(path))
{
var ser = new XmlSerializer(typeof(D2AccountsRoot));
ser.Serialize(stream, root);
}
}
static D2AccountPersisted ToPersistedForXml(D2Account a)
{
string encrypted;
if (!string.IsNullOrEmpty(a?.Password))
encrypted = D2AccountCrypto.Protect(a.Password);
else
encrypted = a?.EncryptedPassword ?? string.Empty;
return new D2AccountPersisted
{
Name = a?.Name,
Email = a?.Email,
EncryptedPassword = encrypted,
Characters = MapCharactersToPersisted(a?.Characters),
};
}
static List<D2Character> MapCharactersFromPersisted(List<D2CharacterPersisted> list)
{
if (list == null || list.Count == 0)
return new List<D2Character>();
var result = new List<D2Character>(list.Count);
foreach (var cp in list)
{
if (cp == null)
continue;
var missions = new List<D2Mission>();
if (cp.Missions != null)
{
foreach (var mp in cp.Missions)
{
if (mp == null)
continue;
missions.Add(new D2Mission
{
Level = mp.Level,
A1_Enpowered = mp.A1_Enpowered,
A5_Socket = mp.A5_Socket,
});
}
}
result.Add(new D2Character { Name = cp.Name, Class = cp.Class, Missions = missions });
}
return result;
}
static List<D2CharacterPersisted> MapCharactersToPersisted(List<D2Character> list)
{
if (list == null || list.Count == 0)
return null;
var result = new List<D2CharacterPersisted>(list.Count);
foreach (var c in list)
{
if (c == null)
continue;
var mpList = new List<D2MissionPersisted>();
if (c.Missions != null)
{
foreach (var m in c.Missions)
{
if (m == null)
continue;
mpList.Add(new D2MissionPersisted
{
Level = m.Level,
A1_Enpowered = m.A1_Enpowered,
A5_Socket = m.A5_Socket,
});
}
}
result.Add(new D2CharacterPersisted { Name = c.Name, Class = c.Class, Missions = mpList });
}
return result.Count == 0 ? null : result;
}
}
}

103
D2Multi.csproj Normal file
View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{3555BB82-5288-4726-B3D7-306E4D2A55F7}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>D2Multi</RootNamespace>
<AssemblyName>D2Multi</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>D2.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Security" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="D2Account.cs" />
<Compile Include="D2AccountPersisted.cs" />
<Compile Include="D2AccountCrypto.cs" />
<Compile Include="D2AccountsXmlStore.cs" />
<Compile Include="D2RInstanceCheckKiller.cs" />
<Compile Include="CharacterMissionGridRow.cs" />
<Compile Include="CharacterMissionsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="CharacterMissionsForm.Designer.cs">
<DependentUpon>CharacterMissionsForm.cs</DependentUpon>
</Compile>
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Content Include="D2.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

25
D2Multi.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36623.8 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "D2Multi", "D2Multi.csproj", "{3555BB82-5288-4726-B3D7-306E4D2A55F7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3555BB82-5288-4726-B3D7-306E4D2A55F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3555BB82-5288-4726-B3D7-306E4D2A55F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3555BB82-5288-4726-B3D7-306E4D2A55F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3555BB82-5288-4726-B3D7-306E4D2A55F7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4B4E4A93-4265-44F8-8043-49BB854F59B7}
EndGlobalSection
EndGlobal

174
D2RInstanceCheckKiller.cs Normal file
View File

@ -0,0 +1,174 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace D2Multi
{
/// <summary>
/// Closes D2R "DiabloII Check For Other Instances" event handles using Sysinternals Handle64
/// (same approach as Scripts/KillInstanceChecks.ps1). Requires administrator rights for Handle64.
/// </summary>
static class D2RInstanceCheckKiller
{
static readonly Regex PidLine = new Regex(
@"^D2R\.exe\s+pid:\s*(?<pid>\d+)",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
static readonly Regex EventHandleLine = new Regex(
@"^\s*(?<handle>\S+)\s*:\s*Event.*DiabloII Check For Other Instances",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
public static bool IsRunningElevated()
{
using (var id = WindowsIdentity.GetCurrent())
{
var p = new WindowsPrincipal(id);
return p.IsInRole(WindowsBuiltInRole.Administrator);
}
}
/// <summary>
/// Resolves Handle64 path: explicit config path if set and exists, else handle64.exe next to D2R folder.
/// </summary>
public static string ResolveHandle64Path(string d2rDirectory, string configuredPath)
{
if (!string.IsNullOrWhiteSpace(configuredPath))
{
var t = configuredPath.Trim();
if (File.Exists(t))
return t;
}
if (string.IsNullOrEmpty(d2rDirectory))
return null;
var nextToD2r = Path.Combine(d2rDirectory, "handle64.exe");
return File.Exists(nextToD2r) ? nextToD2r : null;
}
/// <summary>
/// Enumerates D2R.exe handles and closes matching instance-check events. Returns true if Handle64 ran and exited successfully (including when nothing needed closing).
/// </summary>
public static bool TryCloseInstanceCheckHandles(string handle64Path, string d2rDirectory, out string detailLog)
{
var log = new StringBuilder();
if (string.IsNullOrEmpty(handle64Path) || !File.Exists(handle64Path))
{
detailLog = "handle64.exe not found.";
return false;
}
string listOutput;
int listExit;
if (!RunHandle64(handle64Path, d2rDirectory, "-accepteula -a -p D2R.exe", out listOutput, out listExit, log))
{
detailLog = log.ToString();
return false;
}
if (listExit != 0)
{
log.AppendLine("List pass exit code: " + listExit);
detailLog = log.ToString();
if(listOutput.Contains("No matching handles found"))
{
return true;
}
return false;
}
var currentPid = "";
var closed = 0;
using (var reader = new StringReader(listOutput ?? string.Empty))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var pm = PidLine.Match(line);
if (pm.Success)
{
currentPid = pm.Groups["pid"].Value;
continue;
}
var hm = EventHandleLine.Match(line);
if (!hm.Success)
continue;
var handleId = hm.Groups["handle"].Value.Trim();
if (string.IsNullOrEmpty(currentPid))
{
log.AppendLine("Skip handle " + handleId + " (no prior D2R.exe pid line).");
continue;
}
log.AppendLine("Closing pid=" + currentPid + " handle=" + handleId);
string closeOut;
int closeExit;
var closeArgs = "-accepteula -p " + currentPid + " -c " + handleId + " -y";
if (!RunHandle64(handle64Path, d2rDirectory, closeArgs, out closeOut, out closeExit, log))
{
detailLog = log.ToString();
return false;
}
if (closeExit != 0)
log.AppendLine("Close exit code: " + closeExit);
closed++;
}
}
if (closed == 0)
log.AppendLine("No 'DiabloII Check For Other Instances' event handles found for D2R.exe.");
detailLog = log.ToString();
return true;
}
static bool RunHandle64(string handle64Path, string workingDirectory, string arguments,
out string stdout, out int exitCode, StringBuilder log)
{
stdout = null;
exitCode = -1;
try
{
var psi = new ProcessStartInfo
{
FileName = handle64Path,
Arguments = arguments,
WorkingDirectory = string.IsNullOrEmpty(workingDirectory) ? Path.GetDirectoryName(handle64Path) : workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using (var p = Process.Start(psi))
{
if (p == null)
{
log.AppendLine("Process.Start returned null.");
return false;
}
var outTask = Task.Run(() => p.StandardOutput.ReadToEnd());
var errTask = Task.Run(() => p.StandardError.ReadToEnd());
if (!p.WaitForExit(120000))
{
try { p.Kill(); } catch { /* ignore */ }
log.AppendLine("Handle64 timed out.");
return false;
}
stdout = outTask.GetAwaiter().GetResult();
var err = errTask.GetAwaiter().GetResult();
exitCode = p.ExitCode;
if (!string.IsNullOrEmpty(err))
log.AppendLine("stderr: " + err.Trim());
}
}
catch (Exception ex)
{
log.AppendLine(ex.Message);
return false;
}
return true;
}
}
}

154
Form1.Designer.cs generated Normal file
View File

@ -0,0 +1,154 @@
namespace D2Multi
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.dgvAccounts = new System.Windows.Forms.DataGridView();
this.btnLaunch = new System.Windows.Forms.Button();
this.cboServer = new System.Windows.Forms.ComboBox();
this.btnAdd = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
this.btnSave = new System.Windows.Forms.Button();
this.txExeAddictional = new System.Windows.Forms.TextBox();
this.btnViewCharacters = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dgvAccounts)).BeginInit();
this.SuspendLayout();
//
// dgvAccounts
//
this.dgvAccounts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvAccounts.Location = new System.Drawing.Point(12, 12);
this.dgvAccounts.Name = "dgvAccounts";
this.dgvAccounts.RowHeadersWidth = 51;
this.dgvAccounts.RowTemplate.Height = 24;
this.dgvAccounts.Size = new System.Drawing.Size(779, 255);
this.dgvAccounts.TabIndex = 0;
//
// btnLaunch
//
this.btnLaunch.Location = new System.Drawing.Point(796, 70);
this.btnLaunch.Name = "btnLaunch";
this.btnLaunch.Size = new System.Drawing.Size(158, 36);
this.btnLaunch.TabIndex = 1;
this.btnLaunch.Text = "Launch";
this.btnLaunch.UseVisualStyleBackColor = true;
this.btnLaunch.Click += new System.EventHandler(this.btnLaunch_Click);
//
// cboServer
//
this.cboServer.FormattingEnabled = true;
this.cboServer.Location = new System.Drawing.Point(797, 12);
this.cboServer.Name = "cboServer";
this.cboServer.Size = new System.Drawing.Size(158, 24);
this.cboServer.TabIndex = 2;
//
// btnAdd
//
this.btnAdd.Location = new System.Drawing.Point(12, 276);
this.btnAdd.Name = "btnAdd";
this.btnAdd.Size = new System.Drawing.Size(100, 32);
this.btnAdd.TabIndex = 3;
this.btnAdd.Text = "Add";
this.btnAdd.UseVisualStyleBackColor = true;
this.btnAdd.Click += new System.EventHandler(this.BtnAdd_Click);
//
// btnDelete
//
this.btnDelete.Location = new System.Drawing.Point(118, 276);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(100, 32);
this.btnDelete.TabIndex = 4;
this.btnDelete.Text = "Delete";
this.btnDelete.UseVisualStyleBackColor = true;
this.btnDelete.Click += new System.EventHandler(this.BtnDelete_Click);
//
// btnSave
//
this.btnSave.Location = new System.Drawing.Point(224, 276);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(100, 32);
this.btnSave.TabIndex = 5;
this.btnSave.Text = "Save";
this.btnSave.UseVisualStyleBackColor = true;
this.btnSave.Click += new System.EventHandler(this.BtnSave_Click);
//
// txExeAddictional
//
this.txExeAddictional.Location = new System.Drawing.Point(797, 42);
this.txExeAddictional.Name = "txExeAddictional";
this.txExeAddictional.Size = new System.Drawing.Size(157, 22);
this.txExeAddictional.TabIndex = 6;
this.txExeAddictional.Text = "-mod PY -txt";
//
// btnViewCharacters
//
this.btnViewCharacters.Location = new System.Drawing.Point(330, 276);
this.btnViewCharacters.Name = "btnViewCharacters";
this.btnViewCharacters.Size = new System.Drawing.Size(158, 32);
this.btnViewCharacters.TabIndex = 7;
this.btnViewCharacters.Text = "View Characters";
this.btnViewCharacters.UseVisualStyleBackColor = true;
this.btnViewCharacters.Click += new System.EventHandler(this.btnViewCharacters_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(966, 315);
this.Controls.Add(this.btnViewCharacters);
this.Controls.Add(this.txExeAddictional);
this.Controls.Add(this.btnSave);
this.Controls.Add(this.btnDelete);
this.Controls.Add(this.btnAdd);
this.Controls.Add(this.cboServer);
this.Controls.Add(this.btnLaunch);
this.Controls.Add(this.dgvAccounts);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "Form1";
this.Text = "D2 Accounts";
this.Load += new System.EventHandler(this.Form1_Load);
((System.ComponentModel.ISupportInitialize)(this.dgvAccounts)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.DataGridView dgvAccounts;
private System.Windows.Forms.Button btnLaunch;
private System.Windows.Forms.ComboBox cboServer;
private System.Windows.Forms.Button btnAdd;
private System.Windows.Forms.Button btnDelete;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.TextBox txExeAddictional;
private System.Windows.Forms.Button btnViewCharacters;
}
}

315
Form1.cs Normal file
View File

@ -0,0 +1,315 @@
using System;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace D2Multi
{
public partial class Form1 : Form
{
readonly string _xmlPath;
readonly string _d2rPath;
BindingList<D2Account> _accounts;
BindingSource _bindingSource;
DataGridViewTextBoxColumn _colPassword;
public Form1()
{
InitializeComponent();
var cfgXml = ConfigurationManager.AppSettings["AccountsXmlPath"];
_xmlPath = string.IsNullOrWhiteSpace(cfgXml)
? D2AccountsXmlStore.DefaultPath
: cfgXml.Trim();
_d2rPath = (ConfigurationManager.AppSettings["D2RExePath"] ?? string.Empty).Trim();
}
void Form1_Load(object sender, EventArgs e)
{
InitBattleNetServerCombo();
var loaded = D2AccountsXmlStore.Load(_xmlPath).OrderBy(a => a.Name).ToList();
_accounts = new BindingList<D2Account>(loaded);
_bindingSource = new BindingSource { DataSource = _accounts };
dgvAccounts.AutoGenerateColumns = false;
dgvAccounts.Columns.Clear();
dgvAccounts.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(D2Account.Name),
HeaderText = "Name",
Width = 160
});
dgvAccounts.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(D2Account.Email),
HeaderText = "Email",
Width = 220
});
// Unbound: two-way bind to Password is unreliable here; sync explicitly from the model and CellValidating.
_colPassword = new DataGridViewTextBoxColumn
{
HeaderText = "Password",
Width = 160,
ReadOnly = false
};
dgvAccounts.Columns.Add(_colPassword);
dgvAccounts.DataSource = _bindingSource;
dgvAccounts.DataBindingComplete += DgvAccounts_DataBindingComplete;
dgvAccounts.CellValidating += DgvAccounts_CellValidating;
dgvAccounts.EditingControlShowing += DgvAccounts_EditingControlShowing;
dgvAccounts.CellPainting += DgvAccounts_CellPainting;
}
void InitBattleNetServerCombo()
{
cboServer.DropDownStyle = ComboBoxStyle.DropDownList;
cboServer.DataSource = new[]
{
new { Label = "America", Host = "us.actual.battle.net" },
new { Label = "Asia", Host = "kr.actual.battle.net" },
new { Label = "Europe", Host = "eu.actual.battle.net" },
};
cboServer.DisplayMember = "Label";
cboServer.ValueMember = "Host";
}
void DgvAccounts_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
PushPasswordFromAccountsToGrid();
}
void DgvAccounts_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (e.RowIndex < 0 || dgvAccounts.Columns[e.ColumnIndex] != _colPassword)
return;
var row = dgvAccounts.Rows[e.RowIndex];
if (row.IsNewRow || !(row.DataBoundItem is D2Account acc))
return;
acc.Password = e.FormattedValue == null ? string.Empty : Convert.ToString(e.FormattedValue);
}
void PushPasswordFromAccountsToGrid()
{
foreach (DataGridViewRow row in dgvAccounts.Rows)
{
if (row.IsNewRow || !(row.DataBoundItem is D2Account acc))
continue;
if (dgvAccounts.IsCurrentCellInEditMode && dgvAccounts.CurrentCell != null
&& dgvAccounts.CurrentCell.RowIndex == row.Index
&& dgvAccounts.CurrentCell.ColumnIndex == _colPassword.Index)
continue;
row.Cells[_colPassword.Index].Value = acc.Password ?? string.Empty;
}
}
void DgvAccounts_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.ColumnIndex < 0 || e.RowIndex < 0)
return;
if (dgvAccounts.Columns[e.ColumnIndex] != _colPassword)
return;
if (e.RowIndex >= dgvAccounts.Rows.Count || dgvAccounts.Rows[e.RowIndex].IsNewRow)
return;
if (dgvAccounts.IsCurrentCellInEditMode && dgvAccounts.CurrentCell != null
&& dgvAccounts.CurrentCell.RowIndex == e.RowIndex
&& dgvAccounts.CurrentCell.ColumnIndex == e.ColumnIndex)
return;
var val = e.Value as string;
e.Paint(e.ClipBounds, DataGridViewPaintParts.All & ~DataGridViewPaintParts.ContentForeground);
if (!string.IsNullOrEmpty(val))
{
var mask = new string('\u2022', Math.Min(val.Length, 32));
var fore = e.State.HasFlag(DataGridViewElementStates.Selected)
? e.CellStyle.SelectionForeColor
: e.CellStyle.ForeColor;
TextRenderer.DrawText(e.Graphics, mask, e.CellStyle.Font, e.CellBounds, fore,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine);
}
e.Handled = true;
}
void DgvAccounts_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
if (dgvAccounts.CurrentCell?.OwningColumn == _colPassword && e.Control is TextBox tb)
{
tb.UseSystemPasswordChar = true;
}
}
void BtnAdd_Click(object sender, EventArgs e)
{
var a = new D2Account { Name = "New account" };
_accounts.Add(a);
_bindingSource.Position = _accounts.Count - 1;
}
void BtnDelete_Click(object sender, EventArgs e)
{
if (_bindingSource.Current is D2Account cur)
{
_accounts.Remove(cur);
}
}
void BtnSave_Click(object sender, EventArgs e)
{
dgvAccounts.EndEdit();
_bindingSource.EndEdit();
try
{
D2AccountsXmlStore.Save(_xmlPath, _accounts.ToList());
MessageBox.Show(this, "Accounts saved.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Save failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
void btnLaunch_Click(object sender, EventArgs e)
{
dgvAccounts.EndEdit();
_bindingSource.EndEdit();
if (!(_bindingSource.Current is D2Account cur))
{
MessageBox.Show(this, "Select an account.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var email = (cur.Email ?? string.Empty).Trim();
var password = cur.Password ?? string.Empty;
if (email.Length == 0 || password.Length == 0)
{
MessageBox.Show(this, "Email and password are required.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var host = Convert.ToString(cboServer.SelectedValue);
if (string.IsNullOrWhiteSpace(host))
{
MessageBox.Show(this, "Select a region.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (!File.Exists(_d2rPath))
{
MessageBox.Show(this, "D2R.exe was not found at:\n" + _d2rPath, "Launch failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var d2rDir = Path.GetDirectoryName(_d2rPath);
if (string.IsNullOrEmpty(d2rDir))
d2rDir = Environment.CurrentDirectory;
var handle64Cfg = (ConfigurationManager.AppSettings["Handle64ExePath"] ?? string.Empty).Trim();
var handle64 = D2RInstanceCheckKiller.ResolveHandle64Path(d2rDir, handle64Cfg);
var killScript = Path.Combine(d2rDir, "KillInstanceChecks.ps1");
if (handle64 != null)
{
if (D2RInstanceCheckKiller.TryCloseInstanceCheckHandles(handle64, d2rDir, out var handleLog))
{
Thread.Sleep(4000);
}
else if (File.Exists(killScript))
{
RunKillInstanceChecksScript(d2rDir, killScript);
}
else
{
MessageBox.Show(this,
"Could not clear D2R instance check (Handle64 failed). Run D2Multi as Administrator, "
+ "or place KillInstanceChecks.ps1 next to D2R.exe.\n\n" + handleLog,
Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
else if (File.Exists(killScript))
{
RunKillInstanceChecksScript(d2rDir, killScript);
}
var extra = (txExeAddictional.Text ?? string.Empty).Trim();
var args = "-username " + QuoteProcessArg(email)
+ " -password " + QuoteProcessArg(password)
+ " -address " + QuoteProcessArg(host);
if (extra.Length > 0)
args += " " + extra;
var psi = new ProcessStartInfo
{
FileName = _d2rPath,
Arguments = args,
WorkingDirectory = d2rDir,
UseShellExecute = false,
};
try
{
Process.Start(psi);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Launch failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
void RunKillInstanceChecksScript(string d2rDir, string killScript)
{
var killPsi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + killScript + "\"",
WorkingDirectory = d2rDir,
UseShellExecute = true,
};
try
{
using (var p = Process.Start(killPsi))
{
if (p != null)
p.WaitForExit(15000);
}
Thread.Sleep(4000);
}
catch (Exception ex)
{
MessageBox.Show(this, "Could not run KillInstanceChecks.ps1:\n" + ex.Message, Text,
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
static string QuoteProcessArg(string value)
{
if (value == null)
value = string.Empty;
if (value.Length == 0)
return "\"\"";
if (value.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
return value;
return "\"" + value.Replace("\"", "\\\"") + "\"";
}
void btnViewCharacters_Click(object sender, EventArgs e)
{
if (!(_bindingSource.Current is D2Account cur))
{
MessageBox.Show(this, "Select an account.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var dlg = new CharacterMissionsForm(cur, cur.Name))
{
dlg.ShowDialog(this);
}
}
}
}

4632
Form1.resx Normal file

File diff suppressed because it is too large Load Diff

22
Program.cs Normal file
View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace D2Multi
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

View File

@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("D2Multi")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("D2Multi")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3555bb82-5288-4726-b3d7-306e4d2a55f7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

71
Properties/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace D2Multi.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("D2Multi.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

117
Properties/Resources.resx Normal file
View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

30
Properties/Settings.Designer.cs generated Normal file
View File

@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace D2Multi.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@ -1,2 +0,0 @@
# D2Multi

View File

@ -0,0 +1,63 @@
#== D2R singleclient transparent launcher by Chobot - https://github.com/Chobotz/D2R-multiclient-tools / https://forums.d2jsp.org/user.php?i=1208377 ==================
$bnet_email = 'yuanson.chen@gmail.com'
$bnet_password = 'd1124380788'
#default_region values can be eu/us/kr - default is applied when you do not provide any input and just press enter during region selection
$default_region = 'us'
$launch_mode =''
#======================================== Send me lot of FGs ==========================================================================================================
#============= Check for mandatory components and folder placement =====================================================================
$pc_username = [System.Environment]::UserName
if(![System.IO.File]::Exists("$PSScriptRoot\D2R.exe"))
{
Write-Host "Warning: Script needs to be placed in D2R installation folder. Use lnk shortcut to start it. Follow the installation instructions."
Write-Host "Exiting now."
Read-host "Press ENTER to continue..."
Exit
}
if(![System.IO.File]::Exists("$PSScriptRoot\handle64.exe"))
{
Write-Host "Warning: handle64.exe is missing in the current folder - Follow the installation instructions. You can get it from the Microsoft Official site: https://docs.microsoft.com/en-us/sysinternals/downloads/handle"
Write-Host "Exiting now."
read-host "Press ENTER to continue..."
Exit
}
#============= Preset email address in Bnet launcher ============
$bnet_config_path = "C:\Users\"+$pc_username+"\AppData\Roaming\Battle.net\Battle.net.config"
$new_saved_accounts = "SavedAccountNames`": `"" +$bnet_email +"`","
(Get-Content -Path $bnet_config_path) -replace "SavedAccountNames`": `".+@.+`",",$new_saved_accounts | Set-Content -Path $bnet_config_path
#============= Let the user specify launch mode and region ======
#Do {$launch_mode = Read-Host 'D2R - Select launch mode 1 or 2 (1 - Direct client, 2 - Bnet Launcher, Empty - Direct client)'}
#while ($launch_mode -ne '1' -and $launch_mode -ne '2' -and $launch_mode -ne '')
$region = 'none yet'
if($launch_mode -eq "1" -or $launch_mode -eq "")
{
Do { $region = Read-Host 'Specify region eu/us/kr (no input -> default region)';Write-Host "Selected region: $($region)";}
while ($region -ne 'eu' -and $region -ne 'us' -and $region -ne 'kr' -and $region -ne '')
if($region -eq '')
{
$region = $default_region
}
& "$PSScriptRoot\D2R.exe" -username $bnet_email -password $bnet_password -address $region'.actual.battle.net' -mod PY -txt
Write-Host 'Starting:'$region'.actual.battle.net'
}else {
& "$PSScriptRoot\Diablo II Resurrected Launcher.exe"
}
#read-host "Press ENTER to continue..."

View File

@ -0,0 +1,54 @@
#============= Check for mandatory components and folder placement =====================================================================
$pc_username = [System.Environment]::UserName
if(![System.IO.File]::Exists("$PSScriptRoot\D2R.exe"))
{
Write-Host "Warning: Script needs to be placed in D2R installation folder. Use lnk shortcut to start it. Follow the installation instructions."
Write-Host "Exiting now."
Read-host "Press ENTER to continue..."
Exit
}
if(![System.IO.File]::Exists("$PSScriptRoot\handle64.exe"))
{
Write-Host "Warning: handle64.exe is missing in the current folder - Follow the installation instructions. You can get it from the Microsoft Official site: https://docs.microsoft.com/en-us/sysinternals/downloads/handle"
Write-Host "Exiting now."
read-host "Press ENTER to continue..."
Exit
}
#============= Enumerate D2R process handles and close the D2R instance handle to allow to start other clients - admin required ==========
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; exit }
& "$PSScriptRoot\handle64.exe" -accepteula -a -p D2R.exe > $PSScriptRoot\d2r_handles.txt
$proc_id_populated = ""
$handle_id_populated = ""
foreach($line in Get-Content $PSScriptRoot\d2r_handles.txt) {
$proc_id = $line | Select-String -Pattern '^D2R.exe pid\: (?<g1>.+) ' | %{$_.Matches.Groups[1].value}
if ($proc_id)
{
$proc_id_populated = $proc_id
}
$handle_id = $line | Select-String -Pattern '^(?<g2>.+): Event.*DiabloII Check For Other Instances' | %{$_.Matches.Groups[1].value}
if ($handle_id)
{
$handle_id_populated = $handle_id
}
if($handle_id){
Write-Host "Closing" $proc_id_populated $handle_id_populated
& "$PSScriptRoot\handle64.exe" -p $proc_id_populated -c $handle_id_populated -y
}
}
#read-host "Press ENTER to continue..."