Skip to content

Commit d721d64

Browse files
committed
Add Half-Life !cur_* current contents support
Implements support for Half-Life's !cur_* textures (current water directions) in vmt-bsp, including new CONTENTS_CURRENT_* values and content flag handling. Updates documentation, changelog, and adds a test map and textures for validation. Also introduces a case-insensitive string prefix utility.
1 parent 85cf2c2 commit d721d64

20 files changed

Lines changed: 677 additions & 14 deletions

BUILDING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,12 @@ cmake .. -DCMAKE_BUILD_TYPE=Release
7777
cmake --build . --parallel
7878
```
7979

80-
### macOS 🍎
80+
### macOS (10.15+) 🍎
8181

8282
```bash
8383
# Install dependencies via Homebrew
84-
brew install cmake embree tbb
84+
brew install cmake embree tbb qt@6
85+
# Qt6 is only needed for vmt-lightpreview
8586

8687
# Clone and build
8788
git clone --recursive https://github.com/themuffinator/VibeyMapTools.git

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ For the detailed changelog with full technical details, see the [docs/changelog.
1010

1111
This release continues the VibeyMapTools fork with rebranding and modernization.
1212

13+
### Features
14+
- **vmt-bsp**: Half-Life `!cur_*` current contents support (`CONTENTS_CURRENT_*`)
15+
1316
### Changed
1417
- Project rebranded from ericw-tools to VibeyMapTools (VMT)
1518
- All executables now prefixed with `vmt-` for clarity

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,21 @@ vmt-light -gpu -extra4 -bounce 2 -denoise mymap.bsp
111111

112112
## 🧪 Building from Source
113113

114-
```bash
115-
git clone --recursive https://github.com/themuffinator/VibeyMapTools.git
116-
cd VibeyMapTools && cmake -B build -DCMAKE_BUILD_TYPE=Release
117-
cmake --build build
118-
```
119-
120-
<details>
121-
<summary><b>Requirements</b></summary>
122-
123-
- CMake 3.14+
124-
- C++20 compiler (MSVC 2019+, GCC 9+, Clang 10+)
125-
- Embree 4.x + oneTBB (required for `vmt-light`)
114+
```bash
115+
git clone --recursive https://github.com/themuffinator/VibeyMapTools.git
116+
cd VibeyMapTools && cmake -B build -DCMAKE_BUILD_TYPE=Release
117+
cmake --build build
118+
```
119+
120+
macOS 10.15+ prep: `brew install cmake embree tbb qt@6` (Qt6 only needed for `vmt-lightpreview`)
121+
122+
<details>
123+
<summary><b>Requirements</b></summary>
124+
125+
- CMake 3.14+
126+
- C++20 compiler (MSVC 2019+, GCC 9+, Clang 10+)
127+
- macOS 10.15+ (Catalina) recommended for Apple builds
128+
- Embree 4.x + oneTBB (required for `vmt-light`)
126129

127130
**Optional extras:**
128131
- Qt6 (for `vmt-lightpreview`)

docs/qbsp.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,36 @@ Special Surfaces
630630
original coordinates of the center of the "origin" brush before it was
631631
translated to 0 0 0.
632632

633+
Half-Life BSP Specific
634+
^^^^^^^^^^^^^^^^^^^^^^
635+
636+
.. texture:: !cur_0
637+
!cur_90
638+
!cur_180
639+
!cur_270
640+
!cur_up
641+
!cur_dwn
642+
643+
These prefixes generate water with the given current direction:
644+
645+
===================== =====
646+
Content type Value
647+
===================== =====
648+
CONTENTS_CURRENT_0 -9
649+
CONTENTS_CURRENT_90 -10
650+
CONTENTS_CURRENT_180 -11
651+
CONTENTS_CURRENT_270 -12
652+
CONTENTS_CURRENT_UP -13
653+
CONTENTS_CURRENT_DOWN -14
654+
===================== =====
655+
656+
During the BSP process, they act like water with extra modifier flags.
657+
658+
.. note::
659+
660+
Different from the vanilla HL compiler, we don't generate faces between different ``CONTENTS_CURRENT_*`` textured
661+
volumes.
662+
633663

634664
External Map Prefab Support
635665
---------------------------

src/common/bspfile.cc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ template<gameid_t ID>
182182
struct gamedef_q1_like_t : public gamedef_t
183183
{
184184
public:
185+
bool allows_hl_contents = false;
186+
185187
explicit gamedef_q1_like_t(const char *friendly_name = "quake", const char *base_dir = "ID1")
186188
: gamedef_t(friendly_name, base_dir)
187189
{
@@ -243,6 +245,20 @@ struct gamedef_q1_like_t : public gamedef_t
243245
case CONTENTS_WATER: return contentflags_t::make(EWT_VISCONTENTS_WATER);
244246
case CONTENTS_EMPTY: return contentflags_t::make(EWT_VISCONTENTS_EMPTY);
245247
}
248+
249+
if (allows_hl_contents) {
250+
// clang-format off
251+
switch (native) {
252+
case HL_CONTENTS_CURRENT_0: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_0);
253+
case HL_CONTENTS_CURRENT_90: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_90);
254+
case HL_CONTENTS_CURRENT_180: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_180);
255+
case HL_CONTENTS_CURRENT_270: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_270);
256+
case HL_CONTENTS_CURRENT_UP: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_UP);
257+
case HL_CONTENTS_CURRENT_DOWN: return contentflags_t::make(EWT_VISCONTENTS_WATER | EWT_CFLAG_CURRENT_DOWN);
258+
}
259+
// clang-format on
260+
}
261+
246262
throw std::invalid_argument(fmt::format("create_contents_from_native: unknown Q1 contents {}", native));
247263
}
248264

@@ -265,6 +281,21 @@ struct gamedef_q1_like_t : public gamedef_t
265281
} else if (contents.flags & EWT_VISCONTENTS_SLIME) {
266282
return CONTENTS_SLIME;
267283
} else if (contents.flags & EWT_VISCONTENTS_WATER) {
284+
if (allows_hl_contents) {
285+
if (contents.flags & EWT_CFLAG_CURRENT_0)
286+
return HL_CONTENTS_CURRENT_0;
287+
if (contents.flags & EWT_CFLAG_CURRENT_90)
288+
return HL_CONTENTS_CURRENT_90;
289+
if (contents.flags & EWT_CFLAG_CURRENT_180)
290+
return HL_CONTENTS_CURRENT_180;
291+
if (contents.flags & EWT_CFLAG_CURRENT_270)
292+
return HL_CONTENTS_CURRENT_270;
293+
if (contents.flags & EWT_CFLAG_CURRENT_UP)
294+
return HL_CONTENTS_CURRENT_UP;
295+
if (contents.flags & EWT_CFLAG_CURRENT_DOWN)
296+
return HL_CONTENTS_CURRENT_DOWN;
297+
}
298+
268299
return CONTENTS_WATER;
269300
} else if (contents.flags & EWT_VISCONTENTS_MIST) {
270301
throw std::invalid_argument("EWT_VISCONTENTS_MIST not representable in Q1");
@@ -328,6 +359,29 @@ struct gamedef_q1_like_t : public gamedef_t
328359
liquid_flags = EWT_CFLAG_DETAIL | EWT_CFLAG_TRANSLUCENT;
329360
}
330361

362+
// HL water with current
363+
if (allows_hl_contents) {
364+
contents_int_t current_flag = 0;
365+
366+
if (string_istarts_with(texname, "!cur_0")) {
367+
current_flag = EWT_CFLAG_CURRENT_0;
368+
} else if (string_istarts_with(texname, "!cur_90")) {
369+
current_flag = EWT_CFLAG_CURRENT_90;
370+
} else if (string_istarts_with(texname, "!cur_180")) {
371+
current_flag = EWT_CFLAG_CURRENT_180;
372+
} else if (string_istarts_with(texname, "!cur_270")) {
373+
current_flag = EWT_CFLAG_CURRENT_270;
374+
} else if (string_istarts_with(texname, "!cur_up")) {
375+
current_flag = EWT_CFLAG_CURRENT_UP;
376+
} else if (string_istarts_with(texname, "!cur_dwn")) {
377+
current_flag = EWT_CFLAG_CURRENT_DOWN;
378+
}
379+
380+
if (current_flag) {
381+
return contentflags_t::make(EWT_VISCONTENTS_WATER | liquid_flags | current_flag);
382+
}
383+
}
384+
331385
if (!Q_strncasecmp(texname.data() + 1, "lava", 4)) {
332386
return contentflags_t::make(EWT_VISCONTENTS_LAVA | liquid_flags);
333387
} else if (!Q_strncasecmp(texname.data() + 1, "slime", 5)) {
@@ -466,6 +520,7 @@ struct gamedef_hl_t : public gamedef_q1_like_t<GAME_HALF_LIFE>
466520
: gamedef_q1_like_t("halflife", "VALVE")
467521
{
468522
has_rgb_lightmap = true;
523+
allows_hl_contents = true;
469524
}
470525

471526
std::span<const aabb3d> get_hull_sizes() const override

src/common/cmdlib.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ std::string_view::const_iterator string_ifind(std::string_view haystack, std::st
685685
[](char a, char b) { return tolower(a) == tolower(b); });
686686
}
687687

688+
bool string_istarts_with(std::string_view haystack, std::string_view needle)
689+
{
690+
return string_ifind(haystack, needle) == haystack.begin();
691+
}
692+
688693
bool string_icontains(std::string_view haystack, std::string_view needle)
689694
{
690695
return string_ifind(haystack, needle) != haystack.end();

src/include/common/bspfile_q1.hh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ enum q1_contents_t : int32_t
105105
CONTENTS_MIN = CONTENTS_SKY
106106
};
107107

108+
constexpr static int HL_CONTENTS_CURRENT_0 = -9;
109+
constexpr static int HL_CONTENTS_CURRENT_90 = -10;
110+
constexpr static int HL_CONTENTS_CURRENT_180 = -11;
111+
constexpr static int HL_CONTENTS_CURRENT_270 = -12;
112+
constexpr static int HL_CONTENTS_CURRENT_UP = -13;
113+
constexpr static int HL_CONTENTS_CURRENT_DOWN = -14;
114+
108115
constexpr static int BSPXBRUSHES_CONTENTS_CLIP = -8;
109116

110117
struct bsp29_dnode_t

src/include/common/cmdlib.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ struct natural_case_insensitive_less
9898

9999
std::string_view::const_iterator string_ifind(std::string_view haystack, std::string_view needle);
100100
bool string_icontains(std::string_view haystack, std::string_view needle);
101+
// case-insensitive, checks if `haystack` starts with `needle`
102+
bool string_istarts_with(std::string_view haystack, std::string_view needle);
101103

102104
#include <chrono>
103105

0 commit comments

Comments
 (0)