diff --git a/D2Editor.exe b/D2Editor.exe
index ada8dc47..bdd7a8b1 100644
Binary files a/D2Editor.exe and b/D2Editor.exe differ
diff --git a/d2s_File_Format.md b/d2s_File_Format.md
index dd4836d8..d59eb398 100644
--- a/d2s_File_Format.md
+++ b/d2s_File_Format.md
@@ -15,44 +15,68 @@ is based on.
Each .d2s file starts with a 765 byte header, after which data
is of variable length.
-|Byte | Length | Desc
-|-----|--------|------------
-|0 | 4 | Signature (0xaa55aa55)
-|4 | 4 | [Version ID](#versions)
-|8 | 4 | File size
-|12 | 4 | [Checksum](#checksum)
-|16 | 4 | [Active Weapon](#active-weapon)
-|20 | 16 | [Character Name](#character-name)
-|36 | 1 | [Character Status](#character-status)
-|37 | 1 | [Character Progression](#Character-progression)
-|38 | 2 | ?
-|40 | 1 | [Character Class](#character-class)
-|41 | 2 | ?
-|43 | 1 | [Level](#level)
-|44 | 4 | Created [unix timestamp](https://en.wikipedia.org/wiki/Unix_time)?
-|48 | 4 | Last Played [unix timestamp](https://en.wikipedia.org/wiki/Unix_time)
-|52 | 4 | ?
-|56 | 64 | [Assigned Skills](#AssignedSkills)
-|120 | 4 | Left Mouse
-|124 | 4 | Right Mouse
-|128 | 4 | Left Mouse (weapon switch)
-|132 | 4 | Right Mouse (weapon switch)
-|136 | 32 | [Character Menu Appearance](#character-menu-appearance)
-|168 | 3 | [Difficulty](#difficulty)
-|171 | 4 | Map ID
-|175 | 2 | ?
-|177 | 2 | Merc dead?
-|179 | 4 | Merc seed?
-|183 | 2 | Merc Name ID
-|185 | 2 | Merc Type
-|187 | 4 | Merc Experience
-|191 | 144 | ?
-|335 | 298 | [Quest](#quest)
-|633 | 80 | [Waypoint](#waypoint)
-|713 | 52 | [NPC](#npc)
-|765 | | [Attributes](#attributes)
-| | 30 | [Skills](#skills)
-| | | [Items](#items)
+|Byte | Byte | Byte |
+|'71' |'87' - '89'|'92' - '96'|Byte | Length | Desc
+|-----|-----------|-----------|-----|--------|------------
+| 0 | 0 | 0 | 0 | 4 | Signature (0xaa55aa55)
+| 4 | 4 | 4 | 4 | 4 | [Version ID](#versions)
+| | | 8 | 8 | 4 | File size
+| | | 12 | 12 | 4 | [Checksum](#checksum)
+| | | 16 | 16 | 4 | [Active Weapon](#active-weapon)
+| 8 | 8 | 20 | 20 | 16 | [Character Name](#character-name) for versions '71' - '97', otherwise 0x00
+| 24 | 24 | 36 | 36 | 1 | [Character Status](#character-status)
+| 25 | 25 | 37 | 37 | 1 | [Character Progression](#Character-progression)
+| 26 | 26 | | | 1 | [Active Weapon](#active-weapon) for versions '71' - '89'
+| 27 | 27 | 38 | 38 | 1 | ? always zero
+| 28 | 28 | | | 1 | ? 0x3F for versions '87' - '89', otherwise 0xDD for version '71'
+| 29 | 29 | | | 1 | ? 0x00 for versions '87' - '89', otherwise 0x01 for version '71'
+| 30 | 30 | | | 1 | ? 0x10 for versions '71' - '89'
+| 31 | 31 | | | 1 | ? 0x00 for versions '71' - '89'
+| 32 | 32 | | | 1 | ? 0x82 for versions '71' - '89'
+| 33 | 33 | 39 | 39 | 1 | ? always zero
+| 34 | 34 | 40 | 40 | 1 | [Character Class](#character-class)
+| 35 | 35 | 41 | 41 | 1 | ? 0x10 for versions '92"+, otherwise 0x00 for versions '71' - '89'
+| | | 42 | 42 | 1 | ? 0x1E for versions '92"+
+| 36 | 36 | 43 | 43 | 1 | [Level](#level)
+| 37 | 37 | | | 1 | ? 0x00 for versions '71' - '89'
+| | | 44 | 44 | 4 | Created [unix timestamp](https://en.wikipedia.org/wiki/Unix_time)?
+| | | 48 | 48 | 4 | Last Played [unix timestamp](https://en.wikipedia.org/wiki/Unix_time)
+| | | 52 | 52 | 4 | ? 0xFF all bytes for versions '92"+
+| 38 | 38 | | | 32 | [Character Menu Appearance](#character-menu-appearance) for versions '71' - '89'
+| | | 56 | 56 | 64 | [Assigned Skills](#AssignedSkills) for versions '92"+
+| 70 | 70 | | | 16 | [Assigned Skills](#AssignedSkills) for versions '71' - '89'
+| 86 | 86 | | | 1 | Left Mouse for versions '71' - '89'
+| | |120 |120 | 4 | Left Mouse for versions '92"+
+| 87 | 87 | | | 1 | Right Mouse for versions '71' - '89'
+| | |124 |124 | 4 | Right Mouse for versions '92"+
+| | |128 |128 | 4 | Left Mouse (weapon switch) for versions '92"+
+| | |132 |132 | 4 | Right Mouse (weapon switch) for versions '92"+
+| | |136 |136 | 32 | [Character Menu Appearance](#character-menu-appearance) for versions '92"+
+| 88 | 88 | | | 1 | first 4 bits difficulty, last 4 bits starting act for versions '71' - '89'
+| 89 | 89 | | | 36 | ? 0x00 for versions '71' - '89'
+| | |168 |168 | 3 | [Difficulty](#difficulty)
+|126 |126 |171 |171 | 4 | Map ID
+| | |175 |175 | 2 | ? 0x00 for versions '92"+
+| | |177 |177 | 2 | Merc dead? for versions '92"+
+| | |179 |179 | 4 | Merc seed? for versions '92"+
+| | |183 |183 | 2 | Merc Name ID for versions '92"+
+| | |185 |185 | 2 | Merc Type for versions '92"+
+| | |187 |187 | 4 | Merc Experience for versions '92"+
+| | |191 | | 140 | ? 0x00 for versions '92' - '96'
+| | | |191 | 28 | ? 0x00 for versions '97"+
+| | | |219 | 48 | [D2R Character Menu Appearance](#d2r-character-menu-appearance) for versions '97"+
+| | | |267 | 16 | [Character Name](#character-name) for versions '98'+, otherwise 0x00 for version '97'
+| | | |283 | 48 | ? 0x00 for versions '97"+
+| | |331 |331 | 1 | ? 0x00 for versions '97"+, otherwise 0x01 for versions '92' - '96'
+| | |332 |332 | 3 | ? 0x00 for versions '92"+
+|130 | | | | 286 | [Quest](#quest) for version '71"
+|416 | | | | 12 | ? 0x00 for version '71"
+| |130 |335 |335 | 298 | [Quest](#quest) for versions '87"+
+|428 |428 |633 |633 | 80 | [Waypoint](#waypoint)
+|508 |508 |713 |713 | 52 | [NPC](#npc)
+|560 |560 |765 |765 | | [Attributes](#attributes)
+| | | | | 30 | [Skills](#skills)
+| | | | | | [Items](#items)
### Versions
@@ -63,7 +87,9 @@ File version. The following values are known:
* `89` is standard game v1.08
* `92` is v1.09 (both the standard game and the Expansion Set.)
* `96` is v1.10 - v1.14d
-* `96` is v1.15+ (D2R)
+* `97` is v1.15 (D2R 1.0.x - 1.1.x)
+* `98` is v1.16 (D2R 1.2.x+)
+
### Checksum
@@ -242,6 +268,9 @@ Assigned skills section is a an array of 16 skill ids mapped to a hotkey, each a
32 byte structure which defines how the character looks in the menu
Does not change in-game look
+### D2R Character Menu Appearance
+32 byte structure which defines how the character looks in the menu
+Does not change in-game look
### Difficulty
3 bytes of data that indicates which of the three difficulties the character has unlocked.
@@ -301,7 +330,7 @@ This structure repeats it self 3 times, once for Normal, Nightmare and Hell. The
| 70 | `[6]quest` | All six quests for Act V. |
| 82 | `1` | Set to 1 if you went to Akara to reset your stats already |
| 83 | `1` | Seems to be set to 0x80 after completing the Difficulty Level |
-| 84 | `12` | Some kind of padding after all the quest data. |
+| 84 | `12` | Some kind of padding after all the quest data. |
### Waypoint
@@ -310,7 +339,7 @@ Waypoint data starts with 2 chars "WS" and
6 unknown bytes, always = {0x01, 0x00, 0x00, 0x00, 0x50, 0x00}
Three structures are in place for each difficulty,
-at offsets 641, 665 and 689.
+at offsets 641, 665 and 689. (436, 460 and 484 for versions '71' - '89')
The contents of this structure are as follows
@@ -417,35 +446,201 @@ After this come N items. Each item starts with a basic 14-byte
structure. Many fields in this structure are not "byte-aligned"
and are described by their bit position and sizes.
-Bit | Size | Desc
-----|------|------
-0 | 16 | "JM" (separate from the list header)
-16 | 4 | ?
-20 | 1 | Identified
-21 | 6 | ?
-27 | 1 | Socketed
-28 | 1 | ?
-29 | 1 | Picked up since last save
-30 | 2 | ?
-32 | 1 | Ear
-33 | 1 | Starter Gear
-34 | 3 | ?
-37 | 1 | Compact
-38 | 1 | Ethereal
-39 | 1 | ?
-40 | 1 | Personalized
-41 | 1 | ?
-42 | 1 | [Runeword](#runeword)
-43 | 15 | ?
-58 | 3 | [Parent](#parent)
-61 | 4 | [Equipped](#equipped)
-65 | 4 | Column
-69 | 3 | Row
-72 | 1 | ?
-73 | 3 | [Stash](#parent)
-76 | 4 | ?
-80 | 24 | Type code (3 letters)
-108 | | [Extended Item Data](#extended-item-data)
+ Bit | Bit | Bit | Bit |
+ '71' | '71' | '71' | '71' | Bit |
+15 bytes|26 bytes|27 bytes|31 bytes|'87' - '96'|Bit | Size | Desc
+--------|--------|--------|--------|-----------|----|------|------
+ 0 | 0 | 0 | 0 | 0 | | 16 | "JM" (separate from the list header)
+ 16 | 16 | 16 | 16 | 16 | 0 | 4 | ? 0x00
+ 20 | 20 | 20 | 20 | 20 | 4 | 1 | Identified
+ 21 | 21 | 21 | 21 | 21 | 5 | 6 | ? 0x00
+ 27 | 27 | 27 | 27 | 27 | 11 | 1 | Socketed
+ 28 | 28 | 28 | 28 | 28 | 12 | 1 | ? 0x00
+ 29 | 29 | 29 | 29 | 29 | 13 | 1 | Picked up since last save
+ 30 | 30 | 30 | 30 | 30 | 14 | 2 | ? 0x00
+ | 32 | 32 | | 32 | 16 | 1 | Ear, 0x01 always for version '71' with 26 bytes
+ 32 | | | 32 | | | 1 | ? 0x00
+ 33 | 33 | 33 | 33 | 33 | 17 | 1 | Starter Gear
+ 34 | 34 | 34 | 34 | 34 | 18 | 1 | ? 0x00
+ 35 | 35 | | 35 | | | 2 | ? 0x03
+ | | 35 | | 35 | 19 | 2 | ? 0x00
+ 37 | 37 | | 37 | 37 | 21 | 1 | Compact, 0x01 always for version '71' with 15 bytes
+ | | 37 | | | | 1 | ? 0x00
+ 38 | 38 | 38 | 38 | 38 | 22 | 1 | Ethereal
+ 39 | 39 | 39 | 39 | 39 | 23 | 1 | ? 0x01 for versions '87'+, otherwise 0x00
+ | | | | 40 | 24 | 1 | Personalized
+ 40 | 40 | 40 | 40 | | | 1 | ? 0x00
+ 41 | 41 | 41 | 41 | 41 | 25 | 1 | ? 0x00
+ | | | | 42 | 26 | 1 | [Runeword](#runeword)
+ 41 | 41 | 41 | 41 | 41 | 25 | 1 | ? 0x00
+ 43 | 43 | 43 | 43 | 43 | 27 | 5 | ? 0x00
+
+
+#### Ear Item:
+ Bit | Bit |
+ '71' | '71' |
+26 bytes|27 bytes| Size | Desc
+--------|--------|------|------
+ 48 | 48 | 5 | ? 0x00
+ 53 | | 10 | ? 0x00
+ | 53 | 10 | 0x13B always, Ear Type code
+ 63 | 63 | 3 | ? 0x00
+ 66 | 66 | 5 | Column
+ 71 | 71 | 2 | Row
+ 73 | 73 | 1 | ? 0x00
+ 74 | | 3 | [Stash](#parent)
+ 77 | | 5 | ? 0x00
+ 82 | 74 | 3 | Opponent Class
+ | 77 | 8 | ? 0x00
+ | 85 | 3 | [Stash](#parent)
+ | 88 | 2 | ? 0x00
+ 85 |100 | 8 | Opponent Level
+ 93 |108 |105 | Opponent Name
+198 | | 10 | ? 0x00
+ |213 | 3 | ? 0x00
+
+
+ Bit |
+'87' - '96'|Bit | Size | Desc
+-----------|----|------|------
+ | 32 | 3 | ? 0x00
+ 48 | | 10 | ? 0x00
+ 58 | 35 | 3 | [Parent](#parent), always 0x00
+ 61 | 38 | 4 | [Equipped](#equipped), always 0x00
+ 65 | 42 | 4 | Column
+ 69 | 46 | 3 | Row
+ 72 | 49 | 1 | ? 0x00
+ 73 | 50 | 3 | [Stash](#parent)
+ 76 | 53 | 3 | Opponent Class
+ 79 | 56 | 7 | Opponent Level
+ | 63 |120 | Opponent Name
+ 86 | |105 | Opponent Name
+ |183 | 1 | ? 0x00
+191 | | 7 | ? 0x00
+
+#### Simple Item:
+ Bit | Bit |
+ '71' | '71' |
+15 bytes|27 bytes| Size | Desc
+--------|--------|------|------
+ 48 | | 18 | ? 0x00
+ | 48 | 20 | ? 0x00
+ 66 | | 5 | Column, if socketed, 0x00 always, if stored in belt 4 bits used, 2 belt row and 2 for belt column)
+ | 68 | 10 | Type code, 10 bit integer
+ 71 | | 3 | Row, if socketed, 3 bits used other wise 2 bits, 0x00 always if stored in belt.
+ 74 | | 3 | [Stash](#parent)
+ 77 | | 3 | ? 0x00
+ | 78 | 1 | 0x01 socket or belt if potion
+ | 79 | 42 | ? 0x00
+ 80 | | 2 | ? 0x03
+ 82 | | 30 | Type code (3 letters)
+112 | | 8 | ? 0x00 to pad to 120 bits
+ |121 | 5 | Column, if socketed, 0x00 always, if stored in belt 4 bits used, 2 belt row and 2 for belt column)
+ |126 | 3 | Row, if socketed, 3 bits used other wise 2 bits, 0x00 always if stored in belt.
+ |127 | 76 | ? 0x00 to pad to 120 bits
+ |203 | 8 | [Parent](#parent), if bits 4-8 are 0, then stored and bits 0-3 are for [Stash](#parent)
+ |211 | 5 | ? 0x00 to pad to 216 bits
+
+ Bit |
+'87' - '96'|Bit | Size | Desc
+-----------|---------|------|------
+ | 32 | 3 | ? 0x00
+ 48 | | 10 | ? 0x00
+ 58 | 35 | 3 | [Parent](#parent)
+ 61 | 38 | 4 | [Equipped](#equipped), always 0x00
+ 65 | 42 | 4 | Column
+ 69 | 46 | 3 | Row
+ 72 | 49 | 1 | ? 0x00
+ 73 | 50 | 3 | [Stash](#parent)
+ | 53 | 8-30 | Type code (3 letters)
+ 76 | | 30 | Type code (3 letters)
+106 | 61 - 83 |1 or 3| if quest item, then 2 bits for quest and 1 bit for num sockets, otherise 1 bit for sockets
+ | 62 - 86 | 0-4 | ? 0x00 to pad to 64 or 88 bits
+107 or 109 | | 3-5 | ? 0x00 to pad to 112 bits
+
+
+#### Gold Item (unused):
+ Bit | Bit |
+ '71' | '71' |
+27 bytes|31 bytes| Size | Desc
+--------|--------|------|------
+ | 48 | 10 | ? 0x00
+ | 58 | 30 | Type code (3 letters)
+ 48 | 90 | 16 | ? 0x00
+ 64 | | 4 | ? 0x00
+ 68 | | 10 | Type code, 10 bit integer
+ 78 | | 3 | ? 0x00
+ | 106 | 7 | ? 0x00
+ 81 | 113 | 4 | ? 0x00
+ 85 | 117 | 12 | 12 bit integer holding gold amount
+ 97 | 129 | 24 | ? 0x00
+121 | 153 | 5 | Column
+126 | 158 | 3 | Row
+129 | 161 | 72 | ? 0x00
+203 | 235 | 8 | [Parent](#parent), if bits 4-8 are 0, then stored and bits 0-3 are for [Stash](#parent)
+211 | 243 | 5 | ? 0x00 to pad to 216/248 bits
+
+
+ Bit |
+'87' - '96'|Bit | Size | Desc
+-----------|---------|------|------
+ | 32 | 3 | ? 0x00
+ 48 | | 10 | ? 0x00
+ 58 | 35 | 3 | [Parent](#parent)
+ 61 | 38 | 4 | [Equipped](#equipped), always 0x00
+ 65 | 42 | 4 | Column
+ 69 | 46 | 3 | Row
+ 72 | 49 | 1 | ? 0x00
+ 73 | 50 | 3 | [Stash](#parent)
+ | 53 | 8-30 | Type code (3 letters)
+ 76 | | 30 | Type code (3 letters)
+106 | 61 - 83 |1 or 3| if quest item, then 2 bits for quest and 1 bit for num sockets, otherise 1 bit for sockets
+ | 62 - 86 | 0-4 | ? 0x00 to pad to 64 or 88 bits
+107 or 109 | | 3-5 | ? 0x00 to pad to 112 bits
+
+#### Extended items:
+ Bit | Bit |
+ '71' | '71' |
+27 bytes|31 bytes| Size | Desc
+--------|--------|------|------
+ | 48 | 10 | ? 0x00
+ | 58 | 30 | Type code (3 letters)
+ 48 | 90 | 4 | [Equipped](#equipped)
+ 52 | 94 | 1 | ? 0x00
+ 53 | 95 | 3 | number of socketed items
+ 56 | 98 | 8 | Item Level
+ 64 | | 4 | ? 0x00
+ 68 | | 10 | Type code, 10 bit integer
+ 78 | | 3 | ? 0x00
+ | 106 | 7 | ? 0x00
+ 81 | 113 | 4 | [Quality](#quality)
+ 85 | 117 | 9 | number items stacked
+ 94 | 126 | 11 | ? 0x00
+105 | 137 | 16 | Durability
+121 | 153 | 5 | Column, if socketed, then 0x00 always, if stored in belt, then 4 bits used, 2 for belt row and 2 for belt column)
+126 | 158 | 3 | Row, if socketed, then 3 bits used otherwise 2 bits, 0x00 always if stored in belt.
+129 | 161 | 8 | Set ID, Unique ID, or 0x00 if not part of a Set or Unique
+139 | 171 | 32 | DWA
+171 | 203 | 32 | DWB
+203 | 235 | 8 | [Parent](#parent), if bits 4-8 are 0, then stored and bits 0-3 are for [Stash](#parent)
+211 | 243 | 5 | ? 0x00 to pad to 216/248 bits
+
+ Bit |
+'87' - '96'|Bit | Size | Desc
+-----------|---------|------|------
+ | 32 | 3 | ? 0x00
+ 48 | | 10 | ? 0x00
+ 58 | 35 | 3 | [Parent](#parent)
+ 61 | 38 | 4 | [Equipped](#equipped), always 0x00
+ 65 | 42 | 4 | Column
+ 69 | 46 | 3 | Row
+ 72 | 49 | 1 | ? 0x00
+ 73 | 50 | 3 | [Stash](#parent)
+ | 53 | 8-30 | Type code (3 letters)
+ 76 | | 30 | Type code (3 letters)
+106 | | 2 | ? 0x00
+108 | 61 - 83 | | [Extended Item Data](#extended-item-data)
+
### Extended Item Data
@@ -456,7 +651,7 @@ Items with extended information store bits based on information in the item head
extra 3-bit integer encoding how many sockets the item has.
|Bit | Size | Desc
-----|------|-------
+|----|------|-------
|108 | | [Sockets](#sockets)
| | | [Custom Graphics](#custom-graphics)
| | | [Class Specific](#class-specific)
diff --git a/readme.md b/readme.md
index 24603f5c..85fe6f95 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-# Diablo II Character Editor
+# Diablo II Character Editor
Copyright (c) 2000-2003 By Burton Tsang
Copyright (c) 2021-2022 By Walter Couto
@@ -77,7 +77,7 @@ Check the following site for updates at https://github.com/WalterCouto/D2CE
* [d2s Binary File Format](d2s_File_Format.md)
### Revision History
-**Version 2.16 (May 20, 2022)**
+**Version 2.16 (June 5, 2022)**
- Updated: fix up mercenary for PTR 2.4 changes to Barbarian
- Updated: reorganized item context menu
- Updated: fixed "reload" issue with mercenary data that would not read the items.
@@ -87,6 +87,8 @@ Check the following site for updates at https://github.com/WalterCouto/D2CE
- Added: add support for dragging and dropping items.
- Added: add d2i item files and support for import/export of items. The application supports the standard d2i files that exists today but if the item being exported is a D2R PTR 2.4 ear or personalized item that contains utf-8 characters outside the ASCII range, it will export the d2i file as a v1.16 item which is not the same as the format of existing d2i files.
- Added: add ability to socket and unsocket items
+- Added: add ability to convert character file to a different version via the "Change Version" menu item.
+- Added: add ability to apply runewords to item via the "Apply Runeword" context menu item.
**Version 2.15 (April 26, 2022)**
diff --git a/source/D2Editor.cpp b/source/D2Editor.cpp
index 48aacf10..eee29d32 100644
--- a/source/D2Editor.cpp
+++ b/source/D2Editor.cpp
@@ -18,7 +18,7 @@
Revision History
================
-Version 2.16 (May 24, 2022)
+Version 2.16 (June 5, 2022)
- Updated: fix up mercenary for PTR 2.4 changes to Barbarian
- Updated: reorganized item context menu
- Updated: fixed "reload" issue with mercenary data that would
@@ -38,6 +38,8 @@ Version 2.16 (May 24, 2022)
- Added: add ability to socket and unsocket items
- Added: add ability to convert character file to a different
version via the "Change Version" menu item.
+ - Added: add ability to apply runewords to item via the
+ "Apply Runeword" context menu item.
Version 2.15 (April 26, 2022)
- Updated: Reorganize resources and add txt file to allow for
diff --git a/source/D2Editor.rc b/source/D2Editor.rc
index bd5c9d6b..d8e18a6a 100644
Binary files a/source/D2Editor.rc and b/source/D2Editor.rc differ
diff --git a/source/D2Editor.vcxproj b/source/D2Editor.vcxproj
index 754c1bb2..1a46d3c6 100644
--- a/source/D2Editor.vcxproj
+++ b/source/D2Editor.vcxproj
@@ -249,7 +249,9 @@
+
+
@@ -258,7 +260,7 @@
-
+
@@ -332,7 +334,9 @@
+
+
diff --git a/source/D2Editor.vcxproj.filters b/source/D2Editor.vcxproj.filters
index 187aed83..73e6c3df 100644
--- a/source/D2Editor.vcxproj.filters
+++ b/source/D2Editor.vcxproj.filters
@@ -150,7 +150,7 @@
Header Files
-
+
Header Files
@@ -291,6 +291,12 @@
Header Files\d2ce\Helpers
+
+ Header Files
+
+
+ Header Files
+
@@ -464,6 +470,12 @@
Source Files\d2ce\Helpers
+
+ Source Files
+
+
+ Source Files
+
diff --git a/source/D2ItemsForm.cpp b/source/D2ItemsForm.cpp
index 7548262f..a518d644 100644
--- a/source/D2ItemsForm.cpp
+++ b/source/D2ItemsForm.cpp
@@ -26,6 +26,7 @@
#include "D2AddGemsForm.h"
#include "D2MercenaryForm.h"
#include "D2SharedStashForm.h"
+#include "D2RunewordForm.h"
#include "d2ce/helpers/ItemHelpers.h"
#include
#include
@@ -3470,6 +3471,28 @@ d2ce::ItemFilter CD2ItemsForm::GetCurrItemFilter() const
return filter;
}
//---------------------------------------------------------------------------
+bool CD2ItemsForm::setItemRuneword(d2ce::Item& item, std::uint16_t id)
+{
+ auto preLocationId = item.getLocation();
+ auto preAltPositionId = item.getAltPositionId();
+ auto preEquipId = item.getEquippedId();
+ if (MainForm.setItemRuneword(item, id))
+ {
+ if (preEquipId != d2ce::EnumEquippedId::NONE)
+ {
+ refreshEquipped(preEquipId);
+ }
+ else
+ {
+ refreshGrid(preLocationId, preAltPositionId);
+ }
+
+ return true;
+ }
+
+ return false;
+}
+//---------------------------------------------------------------------------
const d2ce::Item* CD2ItemsForm::GetInvItem(UINT id, UINT offset) const
{
// Make sure we have hit an item
@@ -4190,11 +4213,6 @@ void CD2ItemsForm::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
auto filter(GetCurrItemFilter());
if (CurrItem == nullptr)
{
- if (filter.LocationId == d2ce::EnumItemLocation::EQUIPPED)
- {
- return;
- }
-
CMenu menu;
VERIFY(menu.LoadMenu(IDR_ITEM_MENU));
@@ -4225,6 +4243,14 @@ void CD2ItemsForm::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
}
}
}
+ else if (filter.LocationId == d2ce::EnumItemLocation::EQUIPPED)
+ {
+ auto pos = FindPopupPosition(*pPopup, ID_ITEM_CONTEXT_GPS_CREATOR);
+ if (pos >= 0)
+ {
+ pPopup->RemoveMenu(pos, MF_BYPOSITION);
+ }
+ }
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
return;
@@ -4839,6 +4865,14 @@ void CD2ItemsForm::OnItemContextRemovePersonalization()
//---------------------------------------------------------------------------
void CD2ItemsForm::OnItemContextApplyruneword()
{
+ if (CurrItem == nullptr)
+ {
+ return;
+ }
+
+ CD2RunewordForm dlg(*this);
+ dlg.DoModal();
+ SetFocus();
}
//---------------------------------------------------------------------------
void CD2ItemsForm::OnItemContextImportitem()
diff --git a/source/D2ItemsForm.h b/source/D2ItemsForm.h
index 173870f6..33f47c62 100644
--- a/source/D2ItemsForm.h
+++ b/source/D2ItemsForm.h
@@ -173,6 +173,7 @@ class CD2ItemsForm : public CDialogEx, public CD2ItemToolTipCtrlCallback, public
friend class CD2GemsForm;
friend class CD2AddGemsForm;
friend class CD2SharedStashForm;
+ friend class CD2RunewordForm;
DECLARE_DYNAMIC(CD2ItemsForm)
public:
@@ -351,6 +352,8 @@ class CD2ItemsForm : public CDialogEx, public CD2ItemToolTipCtrlCallback, public
d2ce::ItemFilter GetCurrItemFilter() const;
+ bool setItemRuneword(d2ce::Item& item, std::uint16_t id);
+
// Inherited via CD2ItemToolTipCtrlCallback
const d2ce::Item* GetInvItem(UINT id, UINT offset) const override;
const d2ce::Item* InvHitTest(UINT id, CPoint point, TOOLINFO* pTI = nullptr) const override;
diff --git a/source/D2LevelInfoForm.cpp b/source/D2LevelInfoForm.cpp
index 9086ec34..1d8a88df 100644
--- a/source/D2LevelInfoForm.cpp
+++ b/source/D2LevelInfoForm.cpp
@@ -93,7 +93,7 @@ CD2LevelInfoForm::~CD2LevelInfoForm()
//---------------------------------------------------------------------------
void CD2LevelInfoForm::DoDataExchange(CDataExchange* pDX)
{
- CDialogEx::DoDataExchange(pDX);
+ __super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LEVELINFO_GRID, LevelInfoGrid);
}
//---------------------------------------------------------------------------
diff --git a/source/D2MainForm.cpp b/source/D2MainForm.cpp
index 5646a321..7beb8892 100644
--- a/source/D2MainForm.cpp
+++ b/source/D2MainForm.cpp
@@ -5670,6 +5670,18 @@ bool CD2MainForm::setItemLocation(d2ce::Item& item, d2ce::EnumEquippedId equippe
return false;
}
//---------------------------------------------------------------------------
+bool CD2MainForm::setItemRuneword(d2ce::Item& item, std::uint16_t id)
+{
+ if (CharInfo.setItemRuneword(item, id))
+ {
+ ItemsChanged = true;
+ StatsChanged();
+ return true;
+ }
+
+ return false;
+}
+//---------------------------------------------------------------------------
size_t CD2MainForm::getNumberOfEquippedItems() const
{
return CharInfo.getNumberOfEquippedItems();
diff --git a/source/D2MainForm.h b/source/D2MainForm.h
index 23384993..c024ba0b 100644
--- a/source/D2MainForm.h
+++ b/source/D2MainForm.h
@@ -21,7 +21,7 @@
#pragma once
#include "d2ce/Character.h"
-#include "MainFormConstants.h"
+#include "D2MainFormConstants.h"
#include "resource.h"
#include
#include
@@ -439,6 +439,8 @@ class CD2MainForm : public CDialogEx
bool setItemLocation(d2ce::Item& item, d2ce::EnumAltItemLocation altPositionId, std::uint16_t positionX, std::uint16_t positionY, d2ce::EnumItemInventory invType, const d2ce::Item*& pRemovedItem);
bool setItemLocation(d2ce::Item& item, d2ce::EnumEquippedId equippedId, d2ce::EnumItemInventory invType, const d2ce::Item*& pRemovedItem);
+ bool setItemRuneword(d2ce::Item& item, std::uint16_t id);
+
size_t getNumberOfEquippedItems() const;
const std::vector>& getEquippedItems() const;
diff --git a/source/MainFormConstants.h b/source/D2MainFormConstants.h
similarity index 97%
rename from source/MainFormConstants.h
rename to source/D2MainFormConstants.h
index 927dab2a..56636c60 100644
--- a/source/MainFormConstants.h
+++ b/source/D2MainFormConstants.h
@@ -1,35 +1,35 @@
-/*
- Diablo II Character Editor
- Copyright (C) 2021-2022 Walter Couto
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-*/
-//---------------------------------------------------------------------------
-
-#pragma once
-
-#include "d2ce/Constants.h"
-#include "d2ce/ExperienceConstants.h"
-#include "d2ce/CharacterStatsConstants.h"
-
-constexpr COLORREF EDITED_COLOUR = 0x00FFFF00;
-const COLORREF NORMAL_COLOUR = GetSysColor(COLOR_WINDOW);
-
-constexpr std::uint32_t NORMAL = static_cast>(d2ce::EnumDifficulty::Normal);
-constexpr std::uint32_t NIGHTMARE = static_cast>(d2ce::EnumDifficulty::Nightmare);
-constexpr std::uint32_t HELL = static_cast>(d2ce::EnumDifficulty::Hell);
-
-static const CString SettingsSection("Settings");
-static const CString BackupCharacterOption("Backup Character");
-//---------------------------------------------------------------------------
+/*
+ Diablo II Character Editor
+ Copyright (C) 2021-2022 Walter Couto
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+//---------------------------------------------------------------------------
+
+#pragma once
+
+#include "d2ce/Constants.h"
+#include "d2ce/ExperienceConstants.h"
+#include "d2ce/CharacterStatsConstants.h"
+
+constexpr COLORREF EDITED_COLOUR = 0x00FFFF00;
+const COLORREF NORMAL_COLOUR = GetSysColor(COLOR_WINDOW);
+
+constexpr std::uint32_t NORMAL = static_cast>(d2ce::EnumDifficulty::Normal);
+constexpr std::uint32_t NIGHTMARE = static_cast>(d2ce::EnumDifficulty::Nightmare);
+constexpr std::uint32_t HELL = static_cast>(d2ce::EnumDifficulty::Hell);
+
+static const CString SettingsSection("Settings");
+static const CString BackupCharacterOption("Backup Character");
+//---------------------------------------------------------------------------
diff --git a/source/D2MultiLineListCtrl.cpp b/source/D2MultiLineListCtrl.cpp
new file mode 100644
index 00000000..9132b083
--- /dev/null
+++ b/source/D2MultiLineListCtrl.cpp
@@ -0,0 +1,1007 @@
+/*
+ Diablo II Character Editor
+ Copyright (C) 2022 Walter Couto
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+//---------------------------------------------------------------------------
+
+#include "pch.h"
+#include "D2MultiLineListCtrl.h"
+
+// CD2MultiLineListCtrl
+
+IMPLEMENT_DYNAMIC(CD2MultiLineListCtrl, CListCtrl)
+
+//---------------------------------------------------------------------------
+CD2MultiLineListCtrl::CD2MultiLineListCtrl()
+{
+}
+//---------------------------------------------------------------------------
+CD2MultiLineListCtrl::~CD2MultiLineListCtrl()
+{
+}
+
+BEGIN_MESSAGE_MAP(CD2MultiLineListCtrl, CListCtrl)
+ ON_NOTIFY_REFLECT(LVN_ITEMCHANGED, &CD2MultiLineListCtrl::OnLvnItemchanged)
+ ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CD2MultiLineListCtrl::OnCustomDraw)
+ ON_WM_KILLFOCUS()
+ ON_WM_SETFOCUS()
+END_MESSAGE_MAP()
+
+//---------------------------------------------------------------------------
+DWORD CD2MultiLineListCtrl::SetExtendedStyle(DWORD dwNewStyle)
+{
+ if (dwNewStyle & LVS_EX_GRIDLINES)
+ {
+ m_bDrawGrid = TRUE;
+ dwNewStyle &= ~LVS_EX_GRIDLINES;
+ }
+ else
+ {
+ m_bDrawGrid = FALSE;
+ }
+
+ return __super::SetExtendedStyle(dwNewStyle);;
+}
+//---------------------------------------------------------------------------
+DWORD CD2MultiLineListCtrl::GetExtendedStyle() const
+{
+ auto flags = __super::GetExtendedStyle();
+ if (m_bDrawGrid)
+ {
+ flags |= LVS_EX_GRIDLINES;
+ }
+ else
+ {
+ flags &= ~LVS_EX_GRIDLINES;
+ }
+
+ return flags;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::GetItem(LVITEM* pItem) const
+{
+ if (pItem == nullptr)
+ {
+ return FALSE;
+ }
+
+ auto nItem = pItem->iItem;
+ if (nItem >= 0 && nItem < (int)m_rowData.size())
+ {
+ LVITEM lvItem = *pItem;
+ const auto& rowData = m_rowData[nItem];
+ lvItem.iItem = (int)rowData.startRow;
+ auto numRows = rowData.numRows;
+ if (!__super::GetItem(&lvItem))
+ {
+ return FALSE;
+ }
+
+ *pItem = lvItem;
+ pItem->iItem = nItem;
+ if (((pItem->mask & LVIF_TEXT) != 0) && (pItem->pszText != LPSTR_TEXTCALLBACK))
+ {
+ if (numRows > 1)
+ {
+ const auto& colData = rowData.colData[pItem->iSubItem];
+ _tcsncpy_s(pItem->pszText, pItem->cchTextMax, colData.text.GetString(), colData.text.GetLength());
+ }
+ }
+
+ if ((pItem->mask & LVIF_PARAM) != 0)
+ {
+ pItem->lParam = LPARAM(rowData.dwData);
+ }
+ return TRUE;
+ }
+
+ return __super::GetItem(pItem);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetItem(const LVITEM* pItem)
+{
+ if (pItem == nullptr)
+ {
+ return FALSE;
+ }
+
+ auto nItem = pItem->iItem;
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ if ((pItem->mask & LVIF_PARAM) != 0)
+ {
+ m_rowData[nItem].dwData = pItem->lParam;
+ }
+
+ if (((pItem->mask & LVIF_TEXT) != 0) && (pItem->pszText != LPSTR_TEXTCALLBACK))
+ {
+ SetItemText(nItem, pItem->iSubItem, pItem->pszText);
+ }
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetItem(int nItem, int nSubItem, UINT nMask, LPCTSTR lpszItem, int /*nImage*/, UINT /*nState*/, UINT /*nStateMask*/, LPARAM lParam)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ if ((nMask & LVIF_PARAM) != 0)
+ {
+ m_rowData[nItem].dwData = lParam;
+ }
+
+ if (((nMask & LVIF_TEXT) != 0) && (lpszItem != LPSTR_TEXTCALLBACK))
+ {
+ SetItemText(nItem, nSubItem, lpszItem);
+ }
+
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetItem(int nItem, int nSubItem, UINT nMask, LPCTSTR lpszItem, int /*nImage*/, UINT /*nState*/, UINT /*nStateMask*/, LPARAM lParam, int /*nIndent*/)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ if ((nMask & LVIF_PARAM) != 0)
+ {
+ m_rowData[nItem].dwData = lParam;
+ }
+
+ if (((nMask & LVIF_TEXT) != 0) && (lpszItem != LPSTR_TEXTCALLBACK))
+ {
+ SetItemText(nItem, nSubItem, lpszItem);
+ }
+
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::ShowGridLines(BOOL bShow)
+{
+ DWORD flags = GetExtendedStyle();
+ if (bShow)
+ {
+ flags |= LVS_EX_GRIDLINES;
+ }
+ else
+ {
+ flags &= ~LVS_EX_GRIDLINES;
+ }
+ SetExtendedStyle(flags);
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::GetItemCount() const
+{
+ return (int)m_rowData.size();
+}
+//---------------------------------------------------------------------------
+POSITION CD2MultiLineListCtrl::GetFirstSelectedItemPosition() const
+{
+ return __super::GetFirstSelectedItemPosition();
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::GetNextSelectedItem(POSITION& pos) const
+{
+ int nItem = __super::GetNextSelectedItem(pos);
+ auto rowPos = int(__super::GetItemData(nItem));
+ if (rowPos < 0)
+ {
+ // only the top row is allowed to be selected
+ rowPos = (int)__super::GetItemData(nItem + rowPos);
+ }
+
+ return rowPos;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::GetItemRect(int nItem, LPRECT lpRect, UINT nCode) const
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size() || lpRect == nullptr)
+ {
+ return FALSE;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ auto numRows = int(rowData.numRows);
+ nItem = int(rowData.startRow);
+
+ CRect bounds;
+ __super::GetItemRect(nItem, lpRect, nCode);
+ if (numRows > 1)
+ {
+ __super::GetItemRect(nItem + (numRows - 1), &bounds, nCode);
+ lpRect->bottom = bounds.bottom;
+ }
+
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::InvalidateItemRect(int nItem, BOOL bErase)
+{
+ CRect bounds;
+ if (!GetItemRect(nItem, &bounds, LVIR_BOUNDS))
+ {
+ return FALSE;
+ }
+
+ InvalidateRect(&bounds, bErase);
+ return true;
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::OnLvnItemchanged(NMHDR* pNMHDR, LRESULT* pResult)
+{
+ NM_LISTVIEW* pNMListView = reinterpret_cast(pNMHDR);
+
+ *pResult = 0;
+
+ // forget messages that don't change the state
+ if (pNMListView->uOldState == pNMListView->uNewState)
+ {
+ return;
+ }
+
+ if ((pNMListView->uNewState ^ pNMListView->uOldState) & LVIS_SELECTED)
+ {
+ // selection state changing
+ auto rowPos = int(__super::GetItemData(pNMListView->iItem));
+ if (rowPos < 0)
+ {
+ // should not happen as only top guy should be selected
+ rowPos = int(__super::GetItemData(pNMListView->iItem + rowPos));
+ }
+
+ InvalidateItemRect(rowPos, FALSE);
+ return;
+ }
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
+{
+ *pResult = CDRF_DODEFAULT;
+ LPNMLVCUSTOMDRAW lplvcd = (LPNMLVCUSTOMDRAW)pNMHDR;
+
+ int nItem = int(lplvcd->nmcd.dwItemSpec);
+ int nSubItem = lplvcd->iSubItem;
+ int nRowPos = (int)__super::GetItemData(nItem);
+ int nFirstItemRow = nRowPos;
+ int nLastItemRow = nRowPos;
+ size_t nNumLines = 1;
+ int nCurrentSelection = -1;
+ BOOL isSelected = FALSE;
+ POSITION posSelection;
+ HDC hdc = lplvcd->nmcd.hdc;
+ CString str;
+ CRect boxRect, bounds, rect;
+ CDC* pDC = CDC::FromHandle(hdc);
+ //BOOL wndFocused = (GetFocus() == this);
+
+ switch (lplvcd->nmcd.dwDrawStage)
+ {
+ case CDDS_PREPAINT:
+ *pResult = CDRF_NOTIFYITEMDRAW;
+ return;
+
+ case CDDS_ITEMPREPAINT:
+ if (lplvcd->nmcd.uItemState & CDIS_FOCUS)
+ {
+ lplvcd->nmcd.uItemState &= ~CDIS_FOCUS;
+ }
+ *pResult = CDRF_NOTIFYSUBITEMDRAW | CDRF_NOTIFYPOSTPAINT;
+ return;
+
+ case CDDS_ITEMPREERASE:
+ *pResult = CDRF_NOTIFYSUBITEMDRAW;
+ return;
+
+ case CDDS_ITEMPOSTPAINT:
+ *pResult = CDRF_SKIPDEFAULT;
+ return;
+
+ case CDDS_SUBITEM | CDDS_PREPAINT | CDDS_ITEM:
+ if (nRowPos < 0)
+ {
+ const auto& rowData = m_rowData[__super::GetItemData(nItem + nRowPos)];
+ nFirstItemRow = int(rowData.startRow);
+ nNumLines = rowData.numRows;
+ nLastItemRow = int(nFirstItemRow + nNumLines - 1);
+ }
+ else
+ {
+ const auto& rowData = m_rowData[nRowPos];
+ nFirstItemRow = int(rowData.startRow);
+ nNumLines = rowData.numRows;
+ nLastItemRow = int(nFirstItemRow + nNumLines - 1);
+ }
+
+ // Get background box
+ boxRect = lplvcd->nmcd.rc;
+ __super::GetItemRect(int(lplvcd->nmcd.dwItemSpec), &bounds, LVIR_BOUNDS);
+ boxRect.top = bounds.top;
+ boxRect.bottom = bounds.bottom;
+ if (nSubItem == 0)
+ {
+ CRect lrect;
+ __super::GetItemRect(int(lplvcd->nmcd.dwItemSpec), &lrect, LVIR_LABEL);
+ boxRect.left = lrect.left;
+ boxRect.right = lrect.right;
+ }
+ else
+ {
+ boxRect.right += bounds.left;
+ boxRect.left += bounds.left;
+ }
+
+ // Get selection
+ posSelection = __super::GetFirstSelectedItemPosition();
+ if (posSelection)
+ {
+ nCurrentSelection = __super::GetNextSelectedItem(posSelection);
+ if ((nCurrentSelection >= nFirstItemRow) && (nCurrentSelection <= nLastItemRow))
+ {
+ isSelected = true;
+ }
+ }
+ else
+ {
+ nCurrentSelection = -1;
+ }
+
+ if (isSelected)
+ {
+ lplvcd->clrTextBk = ::GetSysColor(COLOR_HIGHLIGHT);
+ lplvcd->clrText = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
+ }
+ else
+ {
+ lplvcd->clrTextBk = ::GetSysColor(COLOR_WINDOW);
+ lplvcd->clrText = ::GetSysColor(COLOR_WINDOWTEXT);
+ }
+
+ // Get text string
+ str = __super::GetItemText(nItem, nSubItem);
+
+ // Get text box
+ rect = boxRect;
+ rect.left += nSubItem ? 6 : 2;
+
+ // Fill background box
+ {
+ CBrush brush(lplvcd->clrTextBk);
+ pDC->FillRect(boxRect, &brush);
+ }
+
+ // Draw text
+ {
+ auto oldTextColor = pDC->SetTextColor(lplvcd->clrText);
+ pDC->DrawText(str, rect, DT_SINGLELINE | DT_NOPREFIX | DT_LEFT | DT_VCENTER | DT_END_ELLIPSIS);
+ pDC->SetTextColor(oldTextColor);
+ }
+
+ if (m_bDrawGrid)
+ {
+ // Draw grid
+ CPen gridline(PS_SOLID, 1, ::GetSysColor(COLOR_BTNFACE));
+ pDC->SelectObject(gridline);
+ if (nSubItem > 0)
+ {
+ pDC->MoveTo(boxRect.left, boxRect.top);
+ pDC->LineTo(boxRect.left, boxRect.bottom);
+ }
+
+ if (nItem == nLastItemRow)
+ {
+ pDC->MoveTo(boxRect.left, boxRect.bottom - 1);
+ pDC->LineTo(boxRect.right, boxRect.bottom - 1);
+ }
+ }
+ *pResult = CDRF_SKIPDEFAULT;
+ return;
+ }
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::InsertItem(const LVITEM* pItem)
+{
+ if (pItem == nullptr)
+ {
+ return -1;
+ }
+
+ size_t colCount = 1;
+ CHeaderCtrl* pHeader = GetHeaderCtrl();
+ if (pHeader != nullptr)
+ {
+ auto headerCount = pHeader->GetItemCount();
+ if (headerCount > 0)
+ {
+ colCount = size_t(headerCount);
+ }
+ }
+
+ int result = -1;
+ if (pItem->iItem >= 0 && pItem->iItem < (int)m_rowData.size())
+ {
+ auto row = int(m_rowData[pItem->iItem].startRow);
+ LVITEM newItem = *pItem;
+ newItem.iItem = row;
+ result = __super::InsertItem(&newItem);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, pItem->iItem);
+
+ auto iter = m_rowData.begin();
+ std::advance(iter, pItem->iItem);
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = pItem->pszText;
+ m_rowData.insert(iter, rowData);
+
+ // adjust row offsets
+ for (size_t i = size_t(pItem->iItem + 1); i < m_rowData.size(); ++i)
+ {
+ ++m_rowData[i].startRow;
+ __super::SetItemData(int(m_rowData[i].startRow), i);
+ }
+ }
+ else
+ {
+ LVITEM lvItem = *pItem;
+ lvItem.iItem = __super::GetItemCount();
+ result = __super::InsertItem(&lvItem);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, m_rowData.size());
+
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = pItem->pszText;
+ m_rowData.push_back(rowData);
+ }
+
+ SetMultilineText(pItem->iItem, pItem->pszText);
+ return result;
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::InsertItem(int nItem, LPCTSTR lpszItem)
+{
+ size_t colCount = 1;
+ CHeaderCtrl* pHeader = GetHeaderCtrl();
+ if (pHeader != nullptr)
+ {
+ auto headerCount = pHeader->GetItemCount();
+ if (headerCount > 0)
+ {
+ colCount = size_t(headerCount);
+ }
+ }
+
+ int result = -1;
+ if (nItem >= 0 && nItem < (int)m_rowData.size())
+ {
+ auto row = int(m_rowData[nItem].startRow);
+ result = __super::InsertItem(row, lpszItem);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, nItem);
+
+ auto iter = m_rowData.begin();
+ std::advance(iter, nItem);
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = lpszItem;
+ m_rowData.insert(iter, rowData);
+
+ // adjust row offsets
+ for (size_t i = size_t(nItem + 1); i < m_rowData.size(); ++i)
+ {
+ ++m_rowData[i].startRow;
+ __super::SetItemData(int(m_rowData[i].startRow), i);
+ }
+ }
+ else
+ {
+ result = __super::InsertItem(nItem, lpszItem);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, m_rowData.size());
+
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = lpszItem;
+ m_rowData.push_back(rowData);
+ }
+
+ SetMultilineText(nItem, lpszItem);
+ return result;
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::InsertItem(int nItem, LPCTSTR lpszItem, int nImage)
+{
+ size_t colCount = 1;
+ CHeaderCtrl* pHeader = GetHeaderCtrl();
+ if (pHeader != nullptr)
+ {
+ auto headerCount = pHeader->GetItemCount();
+ if (headerCount > 0)
+ {
+ colCount = size_t(headerCount);
+ }
+ }
+
+ int result = -1;
+ if (nItem >= 0 && nItem < (int)m_rowData.size())
+ {
+ auto row = int(m_rowData[nItem].startRow);
+ result = __super::InsertItem(row, lpszItem, nImage);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, nItem);
+
+ auto iter = m_rowData.begin();
+ std::advance(iter, nItem);
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = lpszItem;
+ m_rowData.insert(iter, rowData);
+
+ // adjust row offsets
+ for (size_t i = size_t(nItem + 1); i < m_rowData.size(); ++i)
+ {
+ ++m_rowData[i].startRow;
+ __super::SetItemData(int(m_rowData[i].startRow), i);
+ }
+ }
+ else
+ {
+ result = __super::InsertItem(nItem, lpszItem, nImage);
+ if (result == -1)
+ {
+ return result;
+ }
+ __super::SetItemData(result, m_rowData.size());
+
+ RowData rowData;
+ rowData.startRow = result;
+ rowData.colData.resize(colCount);
+ rowData.colData[0].text = lpszItem;
+ m_rowData.push_back(rowData);
+ }
+
+ SetMultilineText(nItem, lpszItem);
+ return result;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::DeleteItem(int nItem)
+{
+ if (nItem >= 0 && nItem < (int)m_rowData.size())
+ {
+ auto rowStart = int(m_rowData[nItem].startRow);
+ auto numRows = m_rowData[nItem].numRows;
+ auto iter = m_rowData.begin();
+ std::advance(iter, nItem);
+ m_rowData.erase(iter);
+
+ // adjust row offsets
+ for (size_t i = size_t(nItem); i < m_rowData.size(); ++i)
+ {
+ --m_rowData[i].startRow;
+ __super::SetItemData(int(m_rowData[i].startRow), i);
+ }
+
+ for (size_t i = 0; i < numRows; ++i)
+ {
+ if (!__super::DeleteItem(rowStart))
+ {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ return __super::DeleteItem(nItem);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::DeleteAllItems()
+{
+ m_rowData.clear();
+ return __super::DeleteAllItems();
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::GetTopIndex() const
+{
+ auto idx = __super::GetTopIndex();
+ if (idx < 0)
+ {
+ return idx;
+ }
+
+
+ return 0;
+}
+//---------------------------------------------------------------------------
+CString CD2MultiLineListCtrl::GetItemText(int nItem, int nSubItem) const
+{
+ if (nSubItem < 0 || nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ if (rowData.colData.size() <= size_t(nSubItem))
+ {
+ return FALSE;
+ }
+
+ return rowData.colData[nSubItem].text;
+}
+//---------------------------------------------------------------------------
+int CD2MultiLineListCtrl::GetItemText(int nItem, int nSubItem, LPTSTR lpszText, int nLen) const
+{
+ if (nSubItem < 0 || nItem < 0 || nItem >= (int)m_rowData.size() || nLen <= 0)
+ {
+ return 0;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ if (rowData.colData.size() <= size_t(nSubItem))
+ {
+ return 0;
+ }
+
+ auto maxSize = min(rowData.colData[nSubItem].text.GetLength(), nLen);
+ _tcsncpy_s(lpszText, nLen, rowData.colData[nSubItem].text.GetString(), rowData.colData[nSubItem].text.GetLength());
+ return maxSize;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetItemText(int nItem, int nSubItem, LPCTSTR lpszText)
+{
+ if (nSubItem < 0 || nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ return SetMultilineText(nItem, lpszText, nSubItem);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetItemData(int nItem, DWORD_PTR dwData)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ m_rowData[nItem].dwData = dwData;
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+DWORD_PTR CD2MultiLineListCtrl::GetItemData(int nItem) const
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return 0;
+ }
+
+ return m_rowData[nItem].dwData;
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::EnsureVisible(int nItem, BOOL bPartialOK)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ if (bPartialOK || rowData.numRows <= 1)
+ {
+ return __super::EnsureVisible(int(rowData.startRow), bPartialOK);
+ }
+
+ __super::EnsureVisible(int(rowData.startRow + rowData.numRows - 1), bPartialOK);
+ return __super::EnsureVisible(int(rowData.startRow), bPartialOK);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::RedrawItems(int nFirst, int nLast)
+{
+ if (m_rowData.empty() || (nFirst > nLast))
+ {
+ return FALSE;
+ }
+
+ if (nFirst < 0)
+ {
+ nFirst = 0;
+ }
+
+ if (nLast >= (int)m_rowData.size())
+ {
+ nLast = int(m_rowData.size() - 1);
+ }
+
+ auto& rowDataFirst = m_rowData[nFirst];
+ auto& rowDataLast = m_rowData[nLast];
+ nFirst = int(rowDataFirst.startRow);
+ nLast = int(rowDataLast.startRow + rowDataLast.numRows - 1);
+ return __super::RedrawItems(nFirst, nLast);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::Update(int nItem)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ nItem = int(rowData.startRow);
+ int nLastItem = int(nItem + (rowData.numRows - 1));
+ BOOL bRet = TRUE;
+ for (auto i = nItem; i <= nLastItem; ++i)
+ {
+ if (!__super::Update(nItem))
+ {
+ bRet = false;
+ }
+ }
+
+ return bRet;
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::AdjustColumnWidths()
+{
+ SetRedraw(FALSE);
+ CHeaderCtrl* pHeaderCtrl = GetHeaderCtrl();
+ int nColumnCount = pHeaderCtrl->GetItemCount();
+ for (int i = 0; i < nColumnCount; i++)
+ {
+ SetColumnWidth(i, LVSCW_AUTOSIZE);
+ int nColumnWidth = GetColumnWidth(i);
+ SetColumnWidth(i, LVSCW_AUTOSIZE_USEHEADER);
+ int nHeaderWidth = GetColumnWidth(i);
+ SetColumnWidth(i, max(nColumnWidth, nHeaderWidth));
+ }
+ SetRedraw(TRUE);
+}
+//---------------------------------------------------------------------------
+BOOL CD2MultiLineListCtrl::SetMultilineText(int nItem, LPCTSTR lpszText, int nSubItem)
+{
+ if (nItem < 0 || nItem >= (int)m_rowData.size())
+ {
+ return FALSE;
+ }
+
+ BOOL bInsert = (nSubItem < 0) ? true : false;
+
+ size_t colCount = 1;
+ CHeaderCtrl* pHeader = GetHeaderCtrl();
+ if (pHeader != nullptr)
+ {
+ auto headerCount = pHeader->GetItemCount();
+ if (headerCount > 0)
+ {
+ colCount = size_t(headerCount);
+ }
+ }
+
+ if (nSubItem >= (int)colCount)
+ {
+ return FALSE;
+ }
+
+ auto& rowData = m_rowData[nItem];
+ auto& colData = rowData.colData[bInsert ? 0 : nSubItem];
+ colData.text = lpszText;
+
+ CString str(lpszText);
+ str.Replace(_T("\r\n"), _T("\n"));
+ str.Replace(_T("\r"), _T("\n"));
+
+ CString s;
+ int pos = -1;
+ std::vector strings;
+ while (!str.IsEmpty())
+ {
+ pos = str.Find('\n');
+ if (pos == -1)
+ {
+ s = str;
+ str.Empty();
+ }
+ else
+ {
+ s = str.Left(pos);
+ str = str.Right(str.GetLength() - pos - 1);
+ }
+ strings.push_back(s);
+ }
+
+ auto row = int(rowData.startRow);
+ auto existingNumLines = rowData.numRows;
+ auto existingNumColLines = colData.numRows;
+ if (strings.empty())
+ {
+ colData.numRows = 1;
+ size_t numRows = 0;
+ for (const auto& col : rowData.colData)
+ {
+ numRows = max(numRows, col.numRows);
+ }
+ rowData.numRows = numRows;
+
+ if (!__super::SetItemText(row, nSubItem, _T("")))
+ {
+ return FALSE;
+ }
+
+ // make sure the other lines are blank
+ for (size_t i = 1; i < existingNumColLines; ++i)
+ {
+ __super::SetItemText(int(row + i), nSubItem, _T(""));
+ }
+
+ for (auto i = rowData.numRows; i < existingNumLines; ++i)
+ {
+ __super::DeleteItem(int(row + rowData.numRows));
+
+ // adjust row offsets
+ for (size_t j = row + 1; j < m_rowData.size(); ++j)
+ {
+ --m_rowData[j].startRow;
+ }
+ }
+ return TRUE;
+ }
+
+ size_t numLines = strings.size();
+ colData.numRows = numLines;
+ size_t line = 1;
+ int newRowNum = row;
+ for (const auto& lineStr : strings)
+ {
+ if (line > existingNumLines)
+ {
+ if (bInsert || (nSubItem == 0))
+ {
+ auto idx = __super::InsertItem(newRowNum, lineStr);
+ if (idx == -1)
+ {
+ return FALSE;
+ }
+ }
+ else
+ {
+ auto idx = __super::InsertItem(newRowNum, _T(""));
+ if (idx == -1)
+ {
+ return FALSE;
+ }
+
+ if (!__super::SetItemText(newRowNum, nSubItem, lineStr))
+ {
+ return FALSE;
+ }
+ }
+ __super::SetItemData(newRowNum, -std::int64_t(line - 1));
+
+ // adjust row offsets
+ for (size_t i = row + 1; i < m_rowData.size(); ++i)
+ {
+ ++m_rowData[i].startRow;
+ }
+ ++rowData.numRows;
+ }
+ else if (bInsert)
+ {
+ if (__super::SetItemText(newRowNum, 0, lineStr) == -1)
+ {
+ return FALSE;
+ }
+ }
+ else
+ {
+ if (__super::SetItemText(newRowNum, nSubItem, lineStr) == -1)
+ {
+ return FALSE;
+ }
+ }
+
+ ++line;
+ ++newRowNum;
+ }
+
+ // make sure the other lines are blank
+ for (auto i = line; i < existingNumColLines; ++i)
+ {
+ __super::SetItemText(int(row + i), nSubItem, _T(""));
+ }
+
+ size_t numRows = 0;
+ for (const auto& col : rowData.colData)
+ {
+ numRows = max(numRows, col.numRows);
+ }
+ rowData.numRows = numRows;
+
+ for (auto i = rowData.numRows; i < existingNumLines; ++i)
+ {
+ __super::DeleteItem(int(row + rowData.numRows));
+
+ // adjust row offsets
+ for (size_t j = row + 1; j < m_rowData.size(); ++j)
+ {
+ --m_rowData[j].startRow;
+ }
+ }
+ return TRUE;
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::OnKillFocus(CWnd* pNewWnd)
+{
+ __super::OnKillFocus(pNewWnd);
+
+ auto posSelection = GetFirstSelectedItemPosition();
+ if (posSelection)
+ {
+ CRect clean;
+ CRect bounds;
+ auto rowPos = GetNextSelectedItem(posSelection);
+ if (rowPos >= 0)
+ {
+ // make sure to invalidate all rows
+ InvalidateItemRect(rowPos, FALSE);
+ }
+ }
+}
+//---------------------------------------------------------------------------
+void CD2MultiLineListCtrl::OnSetFocus(CWnd* pOldWnd)
+{
+ CListCtrl::OnSetFocus(pOldWnd);
+
+ auto posSelection = GetFirstSelectedItemPosition();
+ if (posSelection)
+ {
+ CRect clean;
+ CRect bounds;
+ auto rowPos = GetNextSelectedItem(posSelection);
+ if (rowPos >= 0)
+ {
+ // make sure to invalidate all rows
+ InvalidateItemRect(rowPos, FALSE);
+ }
+ }
+}
+//---------------------------------------------------------------------------
diff --git a/source/D2MultiLineListCtrl.h b/source/D2MultiLineListCtrl.h
new file mode 100644
index 00000000..9b72093b
--- /dev/null
+++ b/source/D2MultiLineListCtrl.h
@@ -0,0 +1,128 @@
+/*
+ Diablo II Character Editor
+ Copyright (C) 2022 Walter Couto
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+//---------------------------------------------------------------------------
+#pragma once
+
+// CD2MultiLineListCtrl
+#include
+#include