openclonk/src/landscape/C4MapScriptAlgo.cpp

522 lines
18 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 1998-2000, Matthes Bender
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2013-2016, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
/* Handles scripted map creation */
#include "C4Include.h"
#include "landscape/C4MapScript.h"
#include "lib/C4Random.h"
C4MapScriptAlgo *FnParAlgo(C4PropList *algo_par);
bool C4MapScriptAlgo::GetXYProps(const C4PropList *props, C4PropertyName k, int32_t *out_xy, bool zero_defaults)
{
// Evaluate property named "k" in proplist props to store two numbers in out_xy:
// If props->k is a single integer, fill both numbers in out_xy with it
// If props->k is an array, check that it contains two numbers and store them in out_xy
if (!props->HasProperty(&Strings.P[k]))
{
if (zero_defaults) out_xy[0] = out_xy[1] = 0;
return false;
}
C4Value val; C4ValueArray *arr;
props->GetProperty(k, &val);
if ((arr = val.getArray()))
{
if (arr->GetSize() != 2)
throw C4AulExecError(FormatString(R"(C4MapScriptAlgo: Expected either integer or array with two integer elements in property "%s".)", Strings.P[k].GetCStr()).getData());
out_xy[0] = arr->GetItem(0).getInt();
out_xy[1] = arr->GetItem(1).getInt();
}
else
{
out_xy[0] = out_xy[1] = val.getInt();
}
return true;
}
C4MapScriptAlgoLayer::C4MapScriptAlgoLayer(const C4PropList *props)
{
// Get MAPALGO_Layer properties
C4PropList *layer_pl = props->GetPropertyPropList(P_Layer);
if (!layer_pl || !(layer = layer_pl->GetMapScriptLayer()))
throw C4AulExecError(R"(C4MapScriptAlgoLayer: Expected layer in "Layer" property.)");
}
bool C4MapScriptAlgoLayer::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
fg = layer->GetPix(x,y,0);
bg = layer->GetBackPix(x,y,0);
// Evaluate MAPALGO_Layer at x,y: Just query pixel in layer. Pixels outside the layer range are zero.
return fg != 0 || bg != 0;
}
C4MapScriptAlgoRndChecker::C4MapScriptAlgoRndChecker(const C4PropList *props)
{
// Get MAPALGO_RndChecker properties
seed = props->GetPropertyInt(P_Seed);
if (!seed) seed = Random(65536);
set_percentage = Clamp(props->GetPropertyInt(P_Ratio), 0, 100);
if (!set_percentage) set_percentage = 50;
checker_wdt = Abs(props->GetPropertyInt(P_Wdt));
if (!checker_wdt) checker_wdt = 10;
checker_hgt = Abs(props->GetPropertyInt(P_Hgt));
if (!checker_hgt) checker_hgt = 10;
C4Value is_fixed_offset_v;
if (props->GetProperty(P_FixedOffset, &is_fixed_offset_v))
is_fixed_offset = is_fixed_offset_v.getBool();
else
is_fixed_offset = false;
}
// Division and modulo operators that always round downwards
// Both assuming b>0
static int32_t divD(int32_t a, int32_t b) { return a/b-(a%b<0); }
static int32_t modD(int32_t a, int32_t b) { return (a>=0)?a%b:b-(-a)%b; }
// Creates a field of random numbers between 0 and scale-1. Returns the value of the field at position x,y
// Function should be deterministic for the same value of x,y, but should look somewhat random wrt neighbouring values of x,y
int32_t QuerySeededRandomField(int32_t seed, int32_t x, int32_t y, int32_t scale)
{
return modD((((seed ^ (x*214013))*214013) ^ (y*214013)), scale);
}
bool C4MapScriptAlgoRndChecker::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_RndChecker at x,y: Query a seeded random field scaled by checker_wdt,checker_hgt
if (!is_fixed_offset) { x+=seed%checker_wdt; y+=((seed*214013)%checker_hgt); }
x = divD(x, checker_wdt); y = divD(y, checker_hgt);
return QuerySeededRandomField(seed, x,y, 100) < set_percentage;
}
C4MapScriptAlgoRect::C4MapScriptAlgoRect(const C4PropList *props)
{
// Get MAPALGO_Rect properties
rect = C4Rect(props->GetPropertyInt(P_X), props->GetPropertyInt(P_Y), props->GetPropertyInt(P_Wdt), props->GetPropertyInt(P_Hgt));
}
bool C4MapScriptAlgoRect::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Rect at x,y: Return 1 for pixels contained in rect, 0 otherwise
return rect.Contains(x, y);
}
C4MapScriptAlgoEllipsis::C4MapScriptAlgoEllipsis(const C4PropList *props)
{
// Get MAPALGO_Ellipsis properties
cx = props->GetPropertyInt(P_X);
cy = props->GetPropertyInt(P_Y);
wdt = Abs(props->GetPropertyInt(P_Wdt));
hgt = Abs(props->GetPropertyInt(P_Hgt));
if (!wdt) wdt = 10;
if (!hgt) hgt = wdt;
}
bool C4MapScriptAlgoEllipsis::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Ellipsis at x,y: Return 1 for pixels within ellipsis, 0 otherwise
// warning: overflows for large values (wdt or hgt >=256)
// but who would draw such large ellipsis anyway?
uint64_t dx = Abs((cx-x)*hgt), dy = Abs((cy-y)*wdt);
return dx*dx+dy*dy < uint64_t(wdt)*wdt*hgt*hgt;
}
C4MapScriptAlgoPolygon::C4MapScriptAlgoPolygon(const C4PropList *props)
{
// Get MAPALGO_Polygon properties
C4Value vptx, vpty;
props->GetProperty(P_X, &vptx); props->GetProperty(P_Y, &vpty);
C4ValueArray *ptx = vptx.getArray(), *pty = vpty.getArray();
if (!ptx || !pty || ptx->GetSize() != pty->GetSize())
throw C4AulExecError(R"(C4MapScriptAlgoPolygon: Expected two equally sized int arrays in properties "X" and "Y".)");
poly.resize(ptx->GetSize());
for (int32_t i=0; i<ptx->GetSize(); ++i)
{
poly[i].x = ptx->GetItem(i).getInt();
poly[i].y = pty->GetItem(i).getInt();
}
wdt = props->GetPropertyInt(P_Wdt);
if (!wdt) wdt = 1;
empty = !!props->GetPropertyInt(P_Empty);
open = !!props->GetPropertyInt(P_Open);
if (open && !empty) throw C4AulExecError("C4MapScriptAlgoPolygon: Only empty polygons may be open.");
}
bool C4MapScriptAlgoPolygon::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Polygon at x,y: Return 1 for pixels within the polygon or its borders, 0 otherwise
int32_t crossings = 0;
for (size_t i=0; i<poly.size(); ++i)
{
Pt pt1 = poly[i];
Pt pt2 = poly[(i+1)%poly.size()];
// check border line distance
int32_t pdx = pt2.x-pt1.x, pdy = pt2.y-pt1.y, dx = x-pt1.x, dy = y-pt1.y;
if (i!=poly.size()-1 || !open)
{
int64_t d = dx*pdy-dy*pdx;
int32_t lsq = (pdx*pdx+pdy*pdy);
if (d*d < wdt*wdt*lsq) // check distance perpendicular to line
{
if (Inside(dx*pdx+dy*pdy, 0, lsq)) // check if point lies within pt1 and pt2
return true; // x/y lies on this line
}
}
// check point distance
if (dx*dx+dy*dy < wdt*wdt) return true; // x/y lies close to edge point
// filling of polygon: point is contained if it crosses an off number of borders
if (!empty && (pt1.y<=y) != (pt2.y<=y)) // crossing vertically?
{
// does line pt1-pt2 intersect line (x,y)-(inf,y)?
crossings += (dx>dy*pdx/pdy);
}
}
// x/y lies inside polygon
return (crossings % 2)==1;
}
C4MapScriptAlgoLines::C4MapScriptAlgoLines(const C4PropList *props)
{
// Get MAPALGO_Lines properties
lx = props->GetPropertyInt(P_X);
ly = props->GetPropertyInt(P_Y);
if (!lx && !ly) throw C4AulExecError(R"(C4MapScriptAlgoLines: Invalid direction vector. Either "X" or "Y" must be nonzero!)");
ox = props->GetPropertyInt(P_OffX);
oy = props->GetPropertyInt(P_OffY);
// use sync-safe distance function to calculate line width
int32_t l = Distance(0,0,lx,ly);
// default distance: double line width, so lines and gaps have same width
distance = props->GetPropertyInt(P_Distance);
if (!distance) distance = l+l; // 1+1=2
// cache for calculation
ll = int64_t(lx)*lx+int64_t(ly)*ly;
dl = int64_t(distance) * l;
}
bool C4MapScriptAlgoLines::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Lines at x,y: Return 1 for pixels contained in lines, 0 for pixels between lines
int64_t ax = int64_t(x)-ox;
int64_t ay = int64_t(y)-oy;
int64_t line_pos = (ax*lx + ay*ly) % dl;
if (line_pos < 0) line_pos += dl;
return line_pos < ll;
}
C4MapScriptAlgoModifier::C4MapScriptAlgoModifier(const C4PropList *props, int32_t min_ops, int32_t max_ops)
{
// Evaluate "Op" property of all algos that take another algo or layer as an operand
// Op may be a proplist or an array of proplists
C4Value vops; int32_t n; C4ValueArray temp_ops;
props->GetProperty(P_Op, &vops);
C4ValueArray *ops = vops.getArray();
if (!ops)
{
C4PropList *op = vops.getPropList();
if (op)
{
temp_ops.SetItem(0, vops);
ops = &temp_ops;
n = 1;
}
}
else
{
n = ops->GetSize();
}
if (!ops || n<min_ops || (max_ops && n>max_ops))
throw C4AulExecError(FormatString(R"(C4MapScriptAlgo: Expected between %d and %d operands in property "Op".)", (int)min_ops, (int)max_ops).getData());
operands.resize(n);
try
{
// can easily crash this by building a recursive prop list
// unfortunately, protecting against that is not trivial
for (int32_t i=0; i<n; ++i)
{
C4MapScriptAlgo *new_algo = FnParAlgo(ops->GetItem(i).getPropList());
if (!new_algo) throw C4AulExecError(FormatString(R"(C4MapScriptAlgo: Operand %d in property "Op" not valid.)", (int)i).getData());
operands[i] = new_algo;
}
}
catch (...)
{
Clear();
throw;
}
}
void C4MapScriptAlgoModifier::Clear()
{
// Child algos are owned by this algo, so delete them
for (auto & operand : operands) delete operand;
operands.clear();
}
bool C4MapScriptAlgoAnd::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_And at x,y:
// Return 0 if any of the operands is 0. Otherwise, returns value of last operand.
bool val=false;
for (auto operand : operands)
if (!(val=(*operand)(x, y, fg, bg)))
return false;
return val;
}
bool C4MapScriptAlgoOr::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Or at x,y:
// Return first nonzero operand
bool val;
for (auto operand : operands)
if ((val=(*operand)(x, y, fg, bg)))
return val;
// If all operands are zero, return zero.
return false;
}
bool C4MapScriptAlgoNot::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Not at x,y:
assert(operands.size()==1);
// Return zero if operand is set and one otherwise
return !(*operands[0])(x, y, fg, bg);
}
bool C4MapScriptAlgoXor::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Xor at x,y:
assert(operands.size()==2);
// If exactly one of the two operands is nonzero, return it. Otherwise, return zero.
uint8_t fg1, bg1, fg2, bg2;
bool v1=(*operands[0])(x,y,fg1,bg1);
bool v2=(*operands[1])(x,y,fg2,bg2);
if ((v1 && v2) || (!v1 && !v2))
return false;
if (v1) { fg = fg1; bg = bg1; return true; }
fg = fg2; bg = bg2;
return true;
}
C4MapScriptAlgoOffset::C4MapScriptAlgoOffset(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Offset properties
ox = props->GetPropertyInt(P_OffX);
oy = props->GetPropertyInt(P_OffY);
}
bool C4MapScriptAlgoOffset::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Offset at x,y:
assert(operands.size()==1);
// Return base layer shifted by ox,oy
return (*operands[0])(x-ox,y-oy, fg, bg);
}
C4MapScriptAlgoScale::C4MapScriptAlgoScale(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Scale properties
sx = props->GetPropertyInt(P_X);
sy = props->GetPropertyInt(P_Y);
if (!sx) sx=100;
if (!sy) sy=100;
cx = props->GetPropertyInt(P_OffX);
cy = props->GetPropertyInt(P_OffY);
}
bool C4MapScriptAlgoScale::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Scale at x,y:
assert(operands.size()==1);
// Return base layer scaled by sx,sy percent from fixed point cx-.5,cy-.5
return (*operands[0])((((x-cx)*2+1)*50-sx/2)/sx+cx,(((y-cy)*2+1)*50-sy/2)/sy+cy, fg, bg);
}
C4MapScriptAlgoRotate::C4MapScriptAlgoRotate(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Rotate properties
int32_t r = props->GetPropertyInt(P_R);
sr=fixtoi(Sin(itofix(r)), Precision);
cr=fixtoi(Cos(itofix(r)), Precision);
ox = props->GetPropertyInt(P_OffX);
oy = props->GetPropertyInt(P_OffY);
}
bool C4MapScriptAlgoRotate::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Rotate at x,y:
assert(operands.size()==1);
// Return base layer rotated by angle r around point ox,oy
x-=ox; y-=oy;
return (*operands[0])(x*cr/Precision-y*sr/Precision+ox,x*sr/Precision+y*cr/Precision+oy, fg, bg);
}
C4MapScriptAlgoTurbulence::C4MapScriptAlgoTurbulence(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Turbulence properties
seed = props->GetPropertyInt(P_Seed);
if (!seed) seed = Random(65536);
GetXYProps(props, P_Amplitude, amp, true);
GetXYProps(props, P_Scale, scale, true);
if (!scale[0]) scale[0] = 10;
if (!scale[1]) scale[1] = 10;
if (!amp[0] && !amp[1]) { amp[0] = amp[1] = 10; }
iterations = props->GetPropertyInt(P_Iterations);
if (!iterations) iterations = 2;
}
bool C4MapScriptAlgoTurbulence::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Turbulence at x,y:
// move by a random offset iterations times
assert(operands.size()==1);
int32_t xy[] = {x, y};
for (int32_t iter=0; iter<iterations; ++iter)
{
int32_t s[2], p[2];
for (int dim=0; dim<2; ++dim)
{
s[dim] = divD(xy[dim], scale[dim]);
p[dim] = modD(xy[dim], scale[dim]);
}
int32_t a[2][2];
for (int dim=0; dim<2; ++dim)
{
int32_t aamp = amp[dim] / (iter+1);
if (!aamp) continue;
for (int dx=0; dx<2; ++dx) for (int dy=0; dy<2; ++dy) a[dx][dy] = QuerySeededRandomField(seed+dim, s[0]+dx, s[1]+dy, aamp) - aamp/2;
int32_t a_interp = a[0][0]*(scale[0]-p[0])*(scale[1]-p[1])
+ a[1][0]*( p[0])*(scale[1]-p[1])
+ a[0][1]*(scale[0]-p[0])*( p[1])
+ a[1][1]*( p[0])*( p[1]);
xy[dim] += a_interp / (scale[0]*scale[1]);
}
}
return (*operands[0])(xy[0],xy[1], fg, bg);
}
void C4MapScriptAlgoBorder::ResolveBorderProps(int32_t *p)
{
// Converts arrays in MAPALGO_Border properties to array of [inner border, outer border]
// Input: Negative values mark outer borders; positive values mark inner borders
int32_t inner=0, outer=0;
for (int32_t i=0; i<2; ++i) if (p[i]>0) inner=p[i]; else if (p[i]<0) outer=-p[i];
p[0] = inner; p[1] = outer;
}
C4MapScriptAlgoBorder::C4MapScriptAlgoBorder(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Border properties
int32_t wdt[2] = {0,0};
// Parameter Wdt fills all directions
int32_t n_borders = 0;
n_borders += GetXYProps(props, P_Wdt, wdt, false);
for (int32_t i=0; i<2; ++i) left[i]=top[i]=right[i]=bottom[i]=wdt[i];
// Individual direction parameters
n_borders += GetXYProps(props, P_Left, left, false);
n_borders += GetXYProps(props, P_Top, top, false);
n_borders += GetXYProps(props, P_Right, right, false);
n_borders += GetXYProps(props, P_Bottom, bottom, false);
// Resolve negative/positive values to inner/outer borders
ResolveBorderProps(left);
ResolveBorderProps(top);
ResolveBorderProps(right);
ResolveBorderProps(bottom);
// If nothing was specified, fill all directions with a default: Draw 1px of outer border
if (!n_borders)
{
left[1] = top[1] = right[1] = bottom[1] = 1;
}
}
bool C4MapScriptAlgoBorder::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Border at x,y: Check if position is at a border of operand layer
// For borders inside operand layer, return the operand material. For outside borders, just return 1. For non-borders, return 0.
// Are we inside or outside?
const C4MapScriptAlgo &l = *operands[0];
bool inside = l(x,y,fg,bg);
// Check four sideways directions
const int32_t *ymove[] = { top, bottom }, *xmove [] ={ left, right };
const int32_t d[] = { -1, +1 };
for (int32_t dir=0; dir<2; ++dir)
{
uint8_t fake_fg, fake_bg;
int32_t hgt = ymove[inside!=!dir][!inside];
for (int32_t dy=0; dy<hgt; ++dy)
if (inside==!l(x,y+d[dir]*(dy+1), fake_fg, fake_bg))
return true;
int32_t wdt = xmove[inside!=!dir][!inside];
for (int32_t dx=0; dx<wdt; ++dx)
if (inside==!l(x+d[dir]*(dx+1),y, fake_fg, fake_bg))
return true;
}
// Not on border
return false;
}
C4MapScriptAlgoFilter::C4MapScriptAlgoFilter(const C4PropList *props) : C4MapScriptAlgoModifier(props,1,1)
{
// Get MAPALGO_Filter properties
C4Value spec;
if (!props->GetProperty(P_Filter, &spec))
throw C4AulExecError("MapScriptAlgoFilter without Filter property.");
filter.Init(spec);
}
bool C4MapScriptAlgoFilter::operator () (int32_t x, int32_t y, uint8_t& fg, uint8_t& bg) const
{
// Evaluate MAPALGO_Filter at x,y:
// Return original color if it's marked to go through filter
bool col = (*operands[0])(x,y, fg, bg);
if (!col) fg = bg = 0;
return filter(fg, bg);
}
C4MapScriptAlgo *FnParAlgo(C4PropList *algo_par)
{
// Convert script function parameter to internal C4MapScriptAlgo class. Also resolve all parameters and nested child algos.
if (!algo_par) return nullptr;
// if algo is a layer, take that directly
C4MapScriptLayer *algo_layer = algo_par->GetMapScriptLayer();
if (algo_layer) return new C4MapScriptAlgoLayer(algo_layer);
// otherwise, determine by proplist parameter "algo"
switch (algo_par->GetPropertyInt(P_Algo))
{
case MAPALGO_Layer: return new C4MapScriptAlgoLayer(algo_par);
case MAPALGO_RndChecker: return new C4MapScriptAlgoRndChecker(algo_par);
case MAPALGO_And: return new C4MapScriptAlgoAnd(algo_par);
case MAPALGO_Or: return new C4MapScriptAlgoOr(algo_par);
case MAPALGO_Xor: return new C4MapScriptAlgoXor(algo_par);
case MAPALGO_Not: return new C4MapScriptAlgoNot(algo_par);
case MAPALGO_Offset: return new C4MapScriptAlgoOffset(algo_par);
case MAPALGO_Scale: return new C4MapScriptAlgoScale(algo_par);
case MAPALGO_Rotate: return new C4MapScriptAlgoRotate(algo_par);
case MAPALGO_Rect: return new C4MapScriptAlgoRect(algo_par);
case MAPALGO_Ellipsis: return new C4MapScriptAlgoEllipsis(algo_par);
case MAPALGO_Polygon: return new C4MapScriptAlgoPolygon(algo_par);
case MAPALGO_Lines: return new C4MapScriptAlgoLines(algo_par);
case MAPALGO_Turbulence: return new C4MapScriptAlgoTurbulence(algo_par);
case MAPALGO_Border: return new C4MapScriptAlgoBorder(algo_par);
case MAPALGO_Filter: return new C4MapScriptAlgoFilter(algo_par);
default:
throw C4AulExecError(FormatString("got invalid algo: %d", algo_par->GetPropertyInt(P_Algo)).getData());
}
return nullptr;
}