September 2017 Update
This commit is contained in:
parent
bba7bfd280
commit
ba8067c3b7
14570 changed files with 153136 additions and 63871 deletions
101
Task/Wireworld/Ceylon/wireworld.ceylon
Normal file
101
Task/Wireworld/Ceylon/wireworld.ceylon
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
abstract class Cell(shared Character char) of emptyCell | head | tail | conductor {
|
||||
|
||||
shared Cell output({Cell*} neighbors) =>
|
||||
switch (this)
|
||||
case (emptyCell) emptyCell
|
||||
case (head) tail
|
||||
case (tail) conductor
|
||||
case (conductor) (neighbors.count(head.equals) in 1..2 then head else conductor);
|
||||
|
||||
string => char.string;
|
||||
}
|
||||
|
||||
object emptyCell extends Cell(' ') {}
|
||||
|
||||
object head extends Cell('H') {}
|
||||
|
||||
object tail extends Cell('t') {}
|
||||
|
||||
object conductor extends Cell('.') {}
|
||||
|
||||
Map<Character,Cell> cellsByChar = map { for (cell in `Cell`.caseValues) cell.char->cell };
|
||||
|
||||
class Wireworld(String data) {
|
||||
|
||||
value lines = data.lines;
|
||||
|
||||
value width = max(lines*.size);
|
||||
value height = lines.size;
|
||||
|
||||
function toIndex(Integer x, Integer y) => x + y * width;
|
||||
|
||||
variable value currentState = Array.ofSize(width * height, emptyCell);
|
||||
variable value nextState = Array.ofSize(width * height, emptyCell);
|
||||
|
||||
for (j->line in lines.indexed) {
|
||||
for (i->char in line.indexed) {
|
||||
currentState[toIndex(i, j)] = cellsByChar[char] else emptyCell;
|
||||
}
|
||||
}
|
||||
|
||||
value emptyGrid = Array.ofSize(width * height, emptyCell);
|
||||
void clear(Array<Cell> cells) => emptyGrid.copyTo(cells);
|
||||
|
||||
shared void update() {
|
||||
clear(nextState);
|
||||
for(j in 0:height) {
|
||||
for(i in 0:width) {
|
||||
if(exists cell = currentState[toIndex(i, j)]) {
|
||||
value nextCell = cell.output(neighborhood(currentState, i, j));
|
||||
nextState[toIndex(i, j)] = nextCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
value temp = currentState;
|
||||
currentState = nextState;
|
||||
nextState = temp;
|
||||
}
|
||||
|
||||
shared void display() {
|
||||
for (row in currentState.partition(width)) {
|
||||
print("".join(row));
|
||||
}
|
||||
}
|
||||
|
||||
shared {Cell*} neighborhood(Array<Cell> grid, Integer x, Integer y) => {
|
||||
for (j in y - 1..y + 1)
|
||||
for (i in x - 1..x + 1)
|
||||
if(i in 0:width && j in 0:height)
|
||||
grid[toIndex(i, j)]
|
||||
}.coalesced;
|
||||
|
||||
}
|
||||
|
||||
shared void run() {
|
||||
value data = "tH.........
|
||||
. .
|
||||
...
|
||||
. .
|
||||
Ht.. ......";
|
||||
|
||||
value world = Wireworld(data);
|
||||
|
||||
variable value generation = 0;
|
||||
|
||||
void display() {
|
||||
print("generation: ``generation``");
|
||||
world.display();
|
||||
}
|
||||
|
||||
display();
|
||||
|
||||
while (true) {
|
||||
if (exists input = process.readLine(), input.lowercased == "q") {
|
||||
return;
|
||||
}
|
||||
world.update();
|
||||
generation++;
|
||||
display();
|
||||
|
||||
}
|
||||
}
|
||||
88
Task/Wireworld/JavaScript/wireworld.js
Normal file
88
Task/Wireworld/JavaScript/wireworld.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
var ctx, sizeW, sizeH, scl = 10, map, tmp;
|
||||
function getNeighbour( i, j ) {
|
||||
var ii, jj, c = 0;
|
||||
for( var b = -1; b < 2; b++ ) {
|
||||
for( var a = -1; a < 2; a++ ) {
|
||||
ii = i + a; jj = j + b;
|
||||
if( ii < 0 || ii >= sizeW || jj < 0 || jj >= sizeH ) continue;
|
||||
if( map[ii][jj] == 1 ) c++;
|
||||
}
|
||||
}
|
||||
return ( c == 1 || c == 2 );
|
||||
}
|
||||
function simulate() {
|
||||
drawWorld();
|
||||
for( var j = 0; j < sizeH; j++ ) {
|
||||
for( var i = 0; i < sizeW; i++ ) {
|
||||
switch( map[i][j] ) {
|
||||
case 0: tmp[i][j] = 0; break;
|
||||
case 1: tmp[i][j] = 2; break;
|
||||
case 2: tmp[i][j] = 3; break;
|
||||
case 3:
|
||||
if( getNeighbour( i, j ) ) tmp[i][j] = 1;
|
||||
else tmp[i][j] = 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
[tmp, map] = [map, tmp];
|
||||
setTimeout( simulate, 200 );
|
||||
}
|
||||
function drawWorld() {
|
||||
ctx.fillStyle = "#000"; ctx.fillRect( 0, 0, sizeW * scl, sizeH * scl );
|
||||
for( var j = 0; j < sizeH; j++ ) {
|
||||
for( var i = 0; i < sizeW; i++ ) {
|
||||
switch( map[i][j] ) {
|
||||
case 0: continue;
|
||||
case 1: ctx.fillStyle = "#03f"; break;
|
||||
case 2: ctx.fillStyle = "#f30"; break;
|
||||
case 3: ctx.fillStyle = "#ff3"; break;
|
||||
}
|
||||
ctx.fillRect( i, j, 1, 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
function openFile( event ) {
|
||||
var input = event.target;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
createWorld( reader.result );
|
||||
};
|
||||
reader.readAsText(input.files[0]);
|
||||
}
|
||||
function createWorld( txt ) {
|
||||
var l = txt.split( "\n" );
|
||||
sizeW = parseInt( l[0] );
|
||||
sizeH = parseInt( l[1] );
|
||||
map = new Array( sizeW );
|
||||
tmp = new Array( sizeW );
|
||||
for( var i = 0; i < sizeW; i++ ) {
|
||||
map[i] = new Array( sizeH );
|
||||
tmp[i] = new Array( sizeH );
|
||||
for( var j = 0; j < sizeH; j++ ) {
|
||||
map[i][j] = tmp[i][j] = 0;
|
||||
}
|
||||
}
|
||||
var t;
|
||||
for( var j = 0; j < sizeH; j++ ) {
|
||||
for( var i = 0; i < sizeW; i++ ) {
|
||||
switch( l[j + 2][i] ) {
|
||||
case " ": t = 0; break;
|
||||
case "H": t = 1; break;
|
||||
case "t": t = 2; break;
|
||||
case ".": t = 3; break;
|
||||
}
|
||||
map[i][j] = t;
|
||||
}
|
||||
}
|
||||
init();
|
||||
}
|
||||
function init() {
|
||||
var canvas = document.createElement( "canvas" );
|
||||
canvas.width = sizeW * scl;
|
||||
canvas.height = sizeH * scl;
|
||||
ctx = canvas.getContext( "2d" );
|
||||
ctx.scale( scl, scl );
|
||||
document.body.appendChild( canvas );
|
||||
simulate();
|
||||
}
|
||||
69
Task/Wireworld/Lua/wireworld.lua
Normal file
69
Task/Wireworld/Lua/wireworld.lua
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
local map = {{'t', 'H', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
|
||||
{'.', ' ', ' ', ' ', '.'},
|
||||
{' ', ' ', ' ', '.', '.', '.'},
|
||||
{'.', ' ', ' ', ' ', '.'},
|
||||
{'H', 't', '.', '.', ' ', '.', '.', '.', '.', '.', '.'}}
|
||||
|
||||
function step(map)
|
||||
local next = {}
|
||||
for i = 1, #map do
|
||||
next[i] = {}
|
||||
for j = 1, #map[i] do
|
||||
next[i][j] = map[i][j]
|
||||
if map[i][j] == "H" then
|
||||
next[i][j] = "t"
|
||||
elseif map[i][j] == "t" then
|
||||
next[i][j] = "."
|
||||
elseif map[i][j] == "." then
|
||||
local count = ((map[i-1] or {})[j-1] == "H" and 1 or 0) +
|
||||
((map[i-1] or {})[j] == "H" and 1 or 0) +
|
||||
((map[i-1] or {})[j+1] == "H" and 1 or 0) +
|
||||
((map[i] or {})[j-1] == "H" and 1 or 0) +
|
||||
((map[i] or {})[j+1] == "H" and 1 or 0) +
|
||||
((map[i+1] or {})[j-1] == "H" and 1 or 0) +
|
||||
((map[i+1] or {})[j] == "H" and 1 or 0) +
|
||||
((map[i+1] or {})[j+1] == "H" and 1 or 0)
|
||||
if count == 1 or count == 2 then
|
||||
next[i][j] = "H"
|
||||
else
|
||||
next[i][j] = "."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return next
|
||||
end
|
||||
|
||||
if not not love then
|
||||
local time, frameTime, size = 0, 0.25, 20
|
||||
local colors = {["."] = {255, 200, 0},
|
||||
["t"] = {255, 0, 0},
|
||||
["H"] = {0, 0, 255}}
|
||||
function love.update(dt)
|
||||
time = time + dt
|
||||
if time > frameTime then
|
||||
time = time - frameTime
|
||||
map = step(map)
|
||||
end
|
||||
end
|
||||
|
||||
function love.draw()
|
||||
for i = 1, #map do
|
||||
for j = 1, #map[i] do
|
||||
love.graphics.setColor(colors[map[i][j]] or {0, 0, 0})
|
||||
love.graphics.rectangle("fill", j*size, i*size, size, size)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for iter = 1, 10 do
|
||||
print("\nstep "..iter.."\n")
|
||||
for i = 1, #map do
|
||||
for j = 1, #map[i] do
|
||||
io.write(map[i][j])
|
||||
end
|
||||
io.write("\n")
|
||||
end
|
||||
map = step(map)
|
||||
end
|
||||
end
|
||||
167
Task/Wireworld/Phix/wireworld.phix
Normal file
167
Task/Wireworld/Phix/wireworld.phix
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
--
|
||||
-- demo\rosetta\Wireworld.exw
|
||||
-- ==========================
|
||||
--
|
||||
-- Invoke with file to read or let it read the one below (if compiled assumes source is in the same directory)
|
||||
--
|
||||
-- Note that tabs in description files are not supported - where necessary spaces can be replaced with _ chars.
|
||||
-- (tab chars in text files should technically always represent (to-next) 8 spaces, but not many editors respect
|
||||
-- that, and instead assume the file will only ever be read by the same program/with matching settings. </rant>)
|
||||
-- (see also demo\edix\src\tabs.e\ExpandTabs() for what you'd need if you knew what tab chars really meant.)
|
||||
--
|
||||
/* -- default description:
|
||||
tH.........
|
||||
.___.
|
||||
___...
|
||||
.___.
|
||||
Ht.. ......
|
||||
*/
|
||||
sequence lines, counts
|
||||
integer longest
|
||||
|
||||
function valid_line(string line, integer l=0)
|
||||
if length(line)=0 then return 0 end if
|
||||
for i=1 to length(line) do
|
||||
integer ch = line[i]
|
||||
if not find(ch," _.tH") then
|
||||
if l and ch='\t' then
|
||||
-- as above
|
||||
printf(1,"error: tab char on line %d\n",{l})
|
||||
{} = wait_key()
|
||||
abort(0)
|
||||
end if
|
||||
return 0
|
||||
end if
|
||||
end for
|
||||
return 1
|
||||
end function
|
||||
|
||||
procedure load_desc()
|
||||
string filename = substitute(command_line()[$],".exe",".exw")
|
||||
integer fn = open(filename,"r")
|
||||
if fn=-1 then
|
||||
printf(1,"error opening %s\n",{filename})
|
||||
{} = wait_key()
|
||||
abort(0)
|
||||
end if
|
||||
sequence text = get_text(fn,GT_LF_STRIPPED)
|
||||
close(fn)
|
||||
lines = {}
|
||||
for i=1 to length(text) do
|
||||
string line = text[i]
|
||||
if valid_line(line) then
|
||||
lines = {line}
|
||||
longest = length(line)
|
||||
for j=i+1 to length(text) do
|
||||
line = text[j]
|
||||
if not valid_line(line,j) then exit end if
|
||||
lines = append(lines,line)
|
||||
if longest<length(line) then
|
||||
longest = length(line)
|
||||
end if
|
||||
end for
|
||||
exit
|
||||
end if
|
||||
end for
|
||||
counts = lines
|
||||
end procedure
|
||||
|
||||
constant dxy = {{-1,-1}, {-1,+0}, {-1,+1},
|
||||
{+0,-1}, {+0,+1},
|
||||
{+1,-1}, {+1,+0}, {+1,+1}}
|
||||
|
||||
procedure set_counts()
|
||||
for y=1 to length(lines) do
|
||||
for x=1 to length(lines[y]) do
|
||||
if lines[y][x]='.' then
|
||||
integer count = 0
|
||||
for k=1 to length(dxy) do
|
||||
integer {cx,cy} = sq_add({x,y},dxy[k])
|
||||
if cy>=1 and cy<=length(lines)
|
||||
and cx>=1 and cx<=length(lines[cy])
|
||||
and lines[cy][cx]='H' then
|
||||
count += 1
|
||||
end if
|
||||
end for
|
||||
counts[y][x] = (count=1 or count=2)
|
||||
end if
|
||||
end for
|
||||
end for
|
||||
end procedure
|
||||
|
||||
include pGUI.e
|
||||
|
||||
Ihandle dlg, canvas, timer
|
||||
cdCanvas cddbuffer, cdcanvas
|
||||
|
||||
function redraw_cb(Ihandle /*ih*/, integer /*posx*/, integer /*posy*/)
|
||||
integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE")
|
||||
integer dx = floor(w/(longest+2))
|
||||
integer dy = floor(h/(length(lines)+2))
|
||||
cdCanvasActivate(cddbuffer)
|
||||
cdCanvasClear(cddbuffer)
|
||||
set_counts()
|
||||
for y=1 to length(lines) do
|
||||
for x=1 to length(lines[y]) do
|
||||
integer c = lines[y][x], colour
|
||||
if find(c," _") then
|
||||
colour = CD_BLACK
|
||||
elsif c='.' then
|
||||
colour = CD_YELLOW
|
||||
if counts[y][x] then
|
||||
lines[y][x] = 'H'
|
||||
end if
|
||||
elsif c='H' then
|
||||
colour = CD_BLUE
|
||||
lines[y][x] = 't'
|
||||
elsif c='t' then
|
||||
colour = CD_RED
|
||||
lines[y][x] = '.'
|
||||
end if
|
||||
cdCanvasSetForeground(cddbuffer, colour)
|
||||
cdCanvasBox(cddbuffer,x*dx,x*dx+dx,h-y*dy,h-(y*dy+dy))
|
||||
end for
|
||||
end for
|
||||
cdCanvasFlush(cddbuffer)
|
||||
return IUP_DEFAULT
|
||||
end function
|
||||
|
||||
function timer_cb(Ihandle /*ih*/)
|
||||
IupUpdate(canvas)
|
||||
return IUP_IGNORE
|
||||
end function
|
||||
|
||||
function map_cb(Ihandle ih)
|
||||
cdcanvas = cdCreateCanvas(CD_IUP, ih)
|
||||
cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
|
||||
cdCanvasSetBackground(cddbuffer, CD_BLACK)
|
||||
return IUP_DEFAULT
|
||||
end function
|
||||
|
||||
function esc_close(Ihandle /*ih*/, atom c)
|
||||
if c=K_ESC then return IUP_CLOSE end if
|
||||
return IUP_CONTINUE
|
||||
end function
|
||||
|
||||
procedure main()
|
||||
load_desc()
|
||||
IupOpen()
|
||||
|
||||
canvas = IupCanvas(NULL)
|
||||
IupSetAttribute(canvas, "RASTERSIZE", "300x180")
|
||||
IupSetCallback(canvas, "MAP_CB", Icallback("map_cb"))
|
||||
IupSetCallback(canvas, "ACTION", Icallback("redraw_cb"))
|
||||
|
||||
timer = IupTimer(Icallback("timer_cb"), 500)
|
||||
|
||||
dlg = IupDialog(canvas)
|
||||
IupSetAttribute(dlg, "TITLE", "Wireworld")
|
||||
IupSetCallback(dlg, "K_ANY", Icallback("esc_close"))
|
||||
|
||||
IupShow(dlg)
|
||||
IupSetAttribute(canvas, "RASTERSIZE", NULL)
|
||||
IupMainLoop()
|
||||
IupClose()
|
||||
end procedure
|
||||
|
||||
main()
|
||||
|
|
@ -1,37 +1,36 @@
|
|||
/*REXX program displays a wire world Cartesian grid of four─state cells. */
|
||||
/*REXX program displays a wire world Cartesian grid of four─state cells. */
|
||||
signal on halt /*handle any cell growth interruptus. */
|
||||
parse arg iFID . '(' generations rows cols bare head tail wire clearScreen reps
|
||||
if iFID=='' then iFID="WIREWORLD.TXT" /*should default input file be used? */
|
||||
blank = 'BLANK' /*the "name" for a blank. */
|
||||
if iFID=='' then iFID= "WIREWORLD.TXT" /*should default input file be used? */
|
||||
bla = 'BLANK' /*the "name" for a blank. */
|
||||
generations = p(generations 100 ) /*number generations that are allowed. */
|
||||
rows = p(rows 3 ) /*the number of cell rows. */
|
||||
cols = p(cols 3 ) /* " " " " columns. */
|
||||
bare = pickChar(bare blank ) /*an empty cell character. */
|
||||
bare = pickChar(bare bla ) /*an empty cell character. */
|
||||
clearScreen = p(clearScreen 0 ) /*1 means to clear the screen. */
|
||||
head = pickChar(head 'H' ) /*pick the character for the head. */
|
||||
tail = pickChar(wire . ) /* " " " " " wire. */
|
||||
tail = pickChar(tail 't' ) /* " " " " " tail. */
|
||||
wire = pickChar(wire . ) /* " " " " " wire. */
|
||||
reps = p(reps 2 ) /*stop program if there are 2 repeats.*/
|
||||
fents=max(linesize()-1,cols) /*the fence width used after displaying*/
|
||||
fents=max(cols, linesize() - 1) /*the fence width used after displaying*/
|
||||
#reps=0; $.=bare /*at start, universe is new and barren.*/
|
||||
gens=abs(generations) /*use for convenience (and short name).*/
|
||||
/* [↓] read the input file. */
|
||||
do r=1 while lines(iFID)\==0 /*keep reading until the End─Of─File. */
|
||||
q=strip(linein(iFID), 'T') /*get single line from the input file. */
|
||||
_=length(q) /*obtain the length of this (input) row*/
|
||||
cols=max(cols, _) /*calculate the maximum number of cols.*/
|
||||
do c=1 for _; $.r.c=substr(q, c, 1); end /*assign the row cells.*/
|
||||
q=strip(linein(iFID),'T'); L=length(q) /*get a line from input file; get len.*/
|
||||
cols=max(cols, L) /*calculate maximum number of columns. */
|
||||
do c=1 for L; $.r.c=substr(q, c, 1); end /*c*/ /*assign the row cells.*/
|
||||
end /*r*/
|
||||
rows=r-1 /*adjust the row number (from DO loop).*/
|
||||
life=0; !.=0; call showCells /*display initial state of the cells. */
|
||||
life=0; !.=0; call showCells /*display initial state of the cells. */
|
||||
/*watch cells evolve, 4 possible states*/
|
||||
do life=1 for gens; @.=bare /*perform for the number of generations*/
|
||||
|
||||
do life=1 for gens; @.=bare /*perform for the number of generations*/
|
||||
do r=1 for rows /*process each of the rows. */
|
||||
do c=1 for cols; ?=$.r.c; ??=? /* " " " " columns. */
|
||||
select /*determine the type of cell. */
|
||||
when ?==head then ??=tail
|
||||
when ?==tail then ??=wire
|
||||
when ?==wire then do; n=hood(); if n==1 | n==2 then ??=head; end
|
||||
when ?==wire then do; #=hood(); if #==1 | #==2 then ??=head; end
|
||||
otherwise nop
|
||||
end /*select*/
|
||||
@.r.c=??
|
||||
|
|
@ -44,21 +43,20 @@ life=0; !.=0; call showCells /*display initial state of the
|
|||
/*stop watching the universe (or life).*/
|
||||
halt: if life-1\==gens then say 'The ~~~Wireworld~~~ program was interrupted.'
|
||||
done: exit /*stick a fork in it, we are all done.*/
|
||||
/*───────────────────────────────────────────────────────────────────────────────────────────────────────────────────*/
|
||||
$: parse arg _row,_col; return $._row._col==head
|
||||
assign$: do r=1 for rows; do c=1 for cols; $.r.c=@.r.c; end; end; return
|
||||
hood: return $(r-1,c-1) + $(r-1,c) + $(r-1,c+1) + $(r,c-1) + $(r,c+1) + $(r+1,c-1) + $(r+1,c) + $(r+1,c+1)
|
||||
p: return word(arg(1), 1) /*pick the 1st word in list.*/
|
||||
pickChar: _=p(arg(1));L=length(_);if translate(_)==bla then _=' ';if L==3 then _=d2c(_);if L==2 then _=x2c(_);return _
|
||||
showRows: _=; do r=1 for rows; z=; do c=1 for cols; z=z||$.r.c; end; z=strip(z,'T'); say z; _=_||z; end; return
|
||||
/*──────────────────────────────────────────────────────────────────────────────────────*/
|
||||
showCells: if clearScreen then 'CLS' /*◄──change this for the OS.*/
|
||||
call showRows /*show rows in proper order.*/
|
||||
say right(copies('═',fents)life,fents) /*display a bunch of cells. */
|
||||
if _=='' then signal done /*No life? Then stop run. */
|
||||
if !._ then #reps=#reps+1 /*detected repeated pattern.*/
|
||||
!._=1 /*it is now existence state.*/
|
||||
say right( copies('═', fents)life, fents) /*display a bunch of cells. */
|
||||
if _=='' then signal done /*No life? Then stop run. */
|
||||
if !._ then #reps=#reps+1 /*detected repeated pattern.*/
|
||||
!._=1 /*it's now existence state. */
|
||||
if reps\==0 & #reps<=reps then return /*so far, so good, no reps. */
|
||||
say '"Wireworld" repeated itself' reps "times, program is stopping."
|
||||
signal done /*exit program, we're done. */
|
||||
/*─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────*/
|
||||
$: parse arg _row,_col; return ($._row._col==head)
|
||||
assign$: do r=1 for rows; do c=1 for cols; $.r.c=@.r.c; end; end; return
|
||||
err: say; say center(' error! ',max(40,linesize()%2),"*"); say; do j=1 for arg(); say arg(j); say; end; say; exit 13
|
||||
hood: return $(r-1,c-1) + $(r-1,c) + $(r-1,c+1) + $(r,c-1) + $(r,c+1) + $(r+1,c-1) + $(r+1,c) + $(r+1,c+1)
|
||||
p: return word(arg(1), 1)
|
||||
pickChar: _=p(arg(1));if translate(_)==blank then _=' ';if length(_)==3 then _=d2c(_);if length(_)==2 then _=x2c(_);return _
|
||||
showRows: _=; do r=1 for rows; z=; do c=1 for cols; z=z || $.r.c; end; z=strip(z,'T'); say z; _=_ || z; end; return
|
||||
say '"Wireworld" repeated itself' reps "times, program is stopping."
|
||||
signal done /*jump to this pgm's "exit".*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue