introduce bitloops
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package puzzle;
|
package puzzle;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@@ -183,6 +184,92 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
static class Clues {
|
||||||
|
|
||||||
|
long lo, hi, vlo, vhi, rlo, rhi;
|
||||||
|
public static Clues createEmpty() {
|
||||||
|
return new Clues(0, 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
public Clues deepCopyGrid() { return new Clues(lo, hi, vlo, vhi, rlo, rhi); }
|
||||||
|
boolean clueless(int idx) {
|
||||||
|
if (!isClue(idx)) return false;
|
||||||
|
clearClue(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public boolean hasRoomForClue(long packed) { return (packed) != X && notClue(packed & 0x7FL) && notClue((packed >>> 7) & 0x7FL); }
|
||||||
|
|
||||||
|
public void setClue(int ri, byte idx) {
|
||||||
|
if ((ri & 64) == 0) {
|
||||||
|
long mask = 1L << ri;
|
||||||
|
lo |= mask;
|
||||||
|
if ((idx & 1) != 0) vlo |= mask; else vlo &= ~mask;
|
||||||
|
if ((idx & 2) != 0) rlo |= mask; else rlo &= ~mask;
|
||||||
|
} else {
|
||||||
|
long mask = 1L << (ri & 63);
|
||||||
|
hi |= mask;
|
||||||
|
if ((idx & 1) != 0) vhi |= mask; else vhi &= ~mask;
|
||||||
|
if ((idx & 2) != 0) rhi |= mask; else rhi &= ~mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public byte digitAt(int idx) {
|
||||||
|
if ((idx & 64) == 0) {
|
||||||
|
int v = (int) ((vlo >>> idx) & 1L);
|
||||||
|
int r = (int) ((rlo >>> idx) & 1L);
|
||||||
|
return (byte) ((r << 1) | v);
|
||||||
|
} else {
|
||||||
|
int v = (int) ((vhi >>> (idx & 63)) & 1L);
|
||||||
|
int r = (int) ((rhi >>> (idx & 63)) & 1L);
|
||||||
|
return (byte) ((r << 1) | v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void clearClue(int idx) {
|
||||||
|
if ((idx & 64) == 0) {
|
||||||
|
long mask = ~(1L << idx);
|
||||||
|
lo &= mask;
|
||||||
|
vlo &= mask;
|
||||||
|
rlo &= mask;
|
||||||
|
} else {
|
||||||
|
long mask = ~(1L << (idx & 63));
|
||||||
|
hi &= mask;
|
||||||
|
vhi &= mask;
|
||||||
|
rhi &= mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public boolean isClue(long index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) != X : ((hi >>> (index & 63)) & 1L) != X; }
|
||||||
|
public boolean isClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) != X : ((hi >>> (index & 63)) & 1L) != X; }
|
||||||
|
public boolean notClue(long index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
||||||
|
public boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
||||||
|
|
||||||
|
public int clueCount() { return Long.bitCount(lo) + Long.bitCount(hi); }
|
||||||
|
public double similarity(Clues b) {
|
||||||
|
long matchLo = (~(lo ^ b.lo)) & (~lo | (~(vlo ^ b.vlo) & ~(rlo ^ b.rlo)));
|
||||||
|
long matchHi = (~(hi ^ b.hi)) & (~hi | (~(vhi ^ b.vhi) & ~(rhi ^ b.rhi)));
|
||||||
|
|
||||||
|
long maskLo = (SIZE >= 64) ? -1L : (1L << SIZE) - 1;
|
||||||
|
long maskHi = (SIZE <= 64) ? 0L : (1L << (SIZE - 64)) - 1;
|
||||||
|
|
||||||
|
return (Long.bitCount(matchLo & maskLo) + Long.bitCount(matchHi & maskHi)) / SIZED;
|
||||||
|
|
||||||
|
|
||||||
|
/* var same = 0;
|
||||||
|
for (int i = 0; i < SIZE; i++) if (digitAt(i) == b.digitAt(i)) same++;
|
||||||
|
return same / SIZED;*/
|
||||||
|
}
|
||||||
|
public Grid toGrid() {
|
||||||
|
var grid = new Grid(new byte[SIZE], lo, hi);
|
||||||
|
for (var l = lo; l != X; l &= l - 1) {
|
||||||
|
int idx = Long.numberOfTrailingZeros(l);
|
||||||
|
grid.g[idx] = digitAt(idx);
|
||||||
|
}
|
||||||
|
for (var h = hi; h != X; h &= h - 1) {
|
||||||
|
int idx = 64 | Long.numberOfTrailingZeros(h);
|
||||||
|
grid.g[idx] = digitAt(idx);
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class Grid {
|
static class Grid {
|
||||||
|
|
||||||
final byte[] g;
|
final byte[] g;
|
||||||
@@ -218,30 +305,9 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
boolean isClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) != X : ((hi >>> (index & 63)) & 1L) != X; }
|
boolean isClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) != X : ((hi >>> (index & 63)) & 1L) != X; }
|
||||||
boolean notClue(long index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
boolean notClue(long index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
||||||
boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
|
||||||
boolean clueless(int idx) {
|
|
||||||
if ((idx & 64) == 0) {
|
|
||||||
val test = (1L << idx);
|
|
||||||
if ((test & lo) == X) return false;
|
|
||||||
g[idx] = DASH;
|
|
||||||
|
|
||||||
lo &= ~test;
|
|
||||||
} else {
|
|
||||||
val test = (1L << (idx & 63));
|
|
||||||
if ((test & hi) == X) return false;
|
|
||||||
g[idx] = DASH;
|
|
||||||
|
|
||||||
hi &= ~test;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double similarity(Grid b) {
|
int clueCount() { return Long.bitCount(lo) + Long.bitCount(hi); }
|
||||||
var same = 0;
|
|
||||||
for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++;
|
|
||||||
return same / SIZED;
|
|
||||||
}
|
|
||||||
int clueCount() { return Long.bitCount(lo) + Long.bitCount(hi); }
|
|
||||||
boolean hasRoomForClue(long packed) { return (packed) != X && notClue(packed & 0x7FL) && notClue((packed >>> 7) & 0x7FL); }
|
|
||||||
void forEachSlot(SlotVisitor visitor) {
|
void forEachSlot(SlotVisitor visitor) {
|
||||||
for (var l = lo; l != X; l &= l - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(l));
|
for (var l = lo; l != X; l &= l - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(l));
|
||||||
for (var h = hi; h != X; h &= h - 1) processSlot(this, visitor, 64 | Long.numberOfTrailingZeros(h));
|
for (var h = hi; h != X; h &= h - 1) processSlot(this, visitor, 64 | Long.numberOfTrailingZeros(h));
|
||||||
@@ -402,7 +468,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// does not modify the grid
|
/// does not modify the grid
|
||||||
long maskFitness(final Grid grid) {
|
long maskFitness(final Clues grid) {
|
||||||
|
|
||||||
long cHLo = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
|
long cHLo = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
|
||||||
long lo_cl = grid.lo, hi_cl = grid.hi;
|
long lo_cl = grid.lo, hi_cl = grid.hi;
|
||||||
@@ -413,6 +479,15 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
for (long bits = (i == 0 ? lo_cl : hi_cl); bits != X; bits &= bits - 1) {
|
for (long bits = (i == 0 ? lo_cl : hi_cl); bits != X; bits &= bits - 1) {
|
||||||
int clueIdx = i | Long.numberOfTrailingZeros(bits);
|
int clueIdx = i | Long.numberOfTrailingZeros(bits);
|
||||||
int key = Slot.packSlotDir(clueIdx, grid.digitAt(clueIdx));
|
int key = Slot.packSlotDir(clueIdx, grid.digitAt(clueIdx));
|
||||||
|
/// long clueBits = (i == 0 ? lo_cl : hi_cl);
|
||||||
|
/// long vBits = (i == 0 ? grid.vlo : grid.vhi);
|
||||||
|
/// long rBits = (i == 0 ? grid.rlo : grid.rhi);
|
||||||
|
/// for (long bits = clueBits; bits != X; bits &= bits - 1) {
|
||||||
|
/// long lsb = bits & -bits;
|
||||||
|
/// int clueIdx = i | Long.numberOfTrailingZeros(lsb);
|
||||||
|
/// int v = (vBits & lsb) != 0 ? 1 : 0;
|
||||||
|
/// int r = (rBits & lsb) != 0 ? 1 : 0;
|
||||||
|
/// int key = Slot.packSlotDir(clueIdx, (r << 1) | v);
|
||||||
long rLo = PATH_LO[key], rHi = PATH_HI[key];
|
long rLo = PATH_LO[key], rHi = PATH_HI[key];
|
||||||
long hLo = rLo & lo_cl, hHi = rHi & hi_cl;
|
long hLo = rLo & lo_cl, hHi = rHi & hi_cl;
|
||||||
if (Slot.increasing(key)) {
|
if (Slot.increasing(key)) {
|
||||||
@@ -531,8 +606,8 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
return penalty;
|
return penalty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Grid randomMask() {
|
SwedishGenerator.Clues randomMask() {
|
||||||
var g = Grid.createEmpty();
|
var g = Clues.createEmpty();
|
||||||
for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) {
|
for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) {
|
||||||
idx = rng.randint(0, SIZE_MIN_1);
|
idx = rng.randint(0, SIZE_MIN_1);
|
||||||
if (g.isClue(idx)) continue;
|
if (g.isClue(idx)) continue;
|
||||||
@@ -544,7 +619,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
}
|
}
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
Grid mutate(Grid grid) {
|
Clues mutate(Clues grid) {
|
||||||
var g = grid.deepCopyGrid();
|
var g = grid.deepCopyGrid();
|
||||||
int ri;
|
int ri;
|
||||||
var bytes = MUTATE_RI[rng.randint(0, SIZE_MIN_1)];
|
var bytes = MUTATE_RI[rng.randint(0, SIZE_MIN_1)];
|
||||||
@@ -557,38 +632,49 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
}
|
}
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
Grid crossover(Grid a, Grid other) {
|
Clues crossover(Clues a, Clues other) {
|
||||||
var out = a.deepCopyGrid();
|
var out = a.deepCopyGrid();
|
||||||
var theta = rng.nextFloat() * Math.PI;
|
var theta = rng.nextFloat() * Math.PI;
|
||||||
var nc = Math.cos(theta);
|
var nc = Math.cos(theta);
|
||||||
var nr = Math.sin(theta);
|
var nr = Math.sin(theta);
|
||||||
|
|
||||||
long bo0 = out.lo, bo1 = out.hi;
|
|
||||||
for (var rci : IT) {
|
for (var rci : IT) {
|
||||||
int i = rci.i();
|
int i = rci.i();
|
||||||
if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
|
if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
|
||||||
byte ch = other.g[i];
|
var ch = other.digitAt(i);
|
||||||
if (out.g[i] != ch) {
|
if (out.digitAt(i) != ch) {
|
||||||
out.g[i] = ch;
|
|
||||||
if (other.isClue(i)) {
|
if (other.isClue(i)) {
|
||||||
if ((i & 64) == 0) bo0 |= (1L << i);
|
out.setClue(i, ch);
|
||||||
else bo1 |= (1L << (i & 63));
|
|
||||||
} else {
|
} else {
|
||||||
if ((i & 64) == 0) bo0 &= ~(1L << i);
|
out.clearClue(i);
|
||||||
else bo1 &= ~(1L << (i & 63));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.lo = bo0;
|
|
||||||
out.hi = bo1;
|
// long maskLo = 0, maskHi = 0;
|
||||||
|
// for (var rci : IT) {
|
||||||
|
// if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
|
||||||
|
// int i = rci.i();
|
||||||
|
// if (i < 64) maskLo |= (1L << i);
|
||||||
|
// else maskHi |= (1L << (i - 64));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// out.lo = (out.lo & ~maskLo) | (other.lo & maskLo);
|
||||||
|
// out.hi = (out.hi & ~maskHi) | (other.hi & maskHi);
|
||||||
|
// out.vlo = (out.vlo & ~maskLo) | (other.vlo & maskLo);
|
||||||
|
// out.vhi = (out.vhi & ~maskHi) | (other.vhi & maskHi);
|
||||||
|
// out.rlo = (out.rlo & ~maskLo) | (other.rlo & maskLo);
|
||||||
|
// out.rhi = (out.rhi & ~maskHi) | (other.rhi & maskHi);
|
||||||
|
|
||||||
for (var lo = out.lo; lo != X; lo &= lo - 1L) clearClues(out, Long.numberOfTrailingZeros(lo));
|
for (var lo = out.lo; lo != X; lo &= lo - 1L) clearClues(out, Long.numberOfTrailingZeros(lo));
|
||||||
for (var hi = out.hi; hi != X; hi &= hi - 1L) clearClues(out, 64 | Long.numberOfTrailingZeros(hi));
|
for (var hi = out.hi; hi != X; hi &= hi - 1L) clearClues(out, 64 | Long.numberOfTrailingZeros(hi));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
public static void clearClues(Grid out, int idx) { if (!out.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(idx, out.digitAt(idx))])) out.clearClue(idx); }
|
public static void clearClues(Clues out, int idx) { if (!out.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(idx, out.digitAt(idx))])) out.clearClue(idx); }
|
||||||
|
|
||||||
Grid hillclimb(Grid start, int limit) {
|
Clues hillclimb(Clues start, int limit) {
|
||||||
var best = start;
|
var best = start;
|
||||||
var bestF = maskFitness(best);
|
var bestF = maskFitness(best);
|
||||||
var fails = 0;
|
var fails = 0;
|
||||||
@@ -607,12 +693,12 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grid generateMask(int popSize, int gens, int pairs) {
|
public SwedishGenerator.Grid generateMask(int popSize, int gens, int pairs) {
|
||||||
class GridAndFit {
|
class GridAndFit {
|
||||||
|
|
||||||
Grid grid;
|
Clues grid;
|
||||||
long fite = -1;
|
long fite = -1;
|
||||||
GridAndFit(Grid grid) { this.grid = grid; }
|
GridAndFit(Clues grid) { this.grid = grid; }
|
||||||
long fit() {
|
long fit() {
|
||||||
if (fite == -1) this.fite = maskFitness(grid);
|
if (fite == -1) this.fite = maskFitness(grid);
|
||||||
return this.fite;
|
return this.fite;
|
||||||
@@ -659,10 +745,10 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
var x = pop.get(i);
|
var x = pop.get(i);
|
||||||
if (x.fit() < best.fit()) best = x;
|
if (x.fit() < best.fit()) best = x;
|
||||||
}
|
}
|
||||||
return best.grid;
|
return best.grid.toGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
static long patternForSlot(Grid grid, Slot s) {
|
static long patternForSlot(Grid grid, SwedishGenerator.Slot s) {
|
||||||
if ((s.lo & ~grid.lo) == 0 && (s.hi & ~grid.hi) == 0) return 0;
|
if ((s.lo & ~grid.lo) == 0 && (s.hi & ~grid.hi) == 0) return 0;
|
||||||
long p = 0;
|
long p = 0;
|
||||||
if (s.increasing()) {
|
if (s.increasing()) {
|
||||||
@@ -769,7 +855,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int[] candidateInfoForPattern(long[] res, long pattern, DictEntry entry, int lenb) {
|
static int[] candidateInfoForPattern(long[] res, long pattern, SwedishGenerator.DictEntry entry, int lenb) {
|
||||||
int numLongs = entry.numlong;
|
int numLongs = entry.numlong;
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
|
|
||||||
@@ -841,7 +927,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
for (int i = 0; i < slots.length; i++) slotScores[i] = slotScore(count, slots[i]);
|
for (int i = 0; i < slots.length; i++) slotScores[i] = slotScore(count, slots[i]);
|
||||||
|
|
||||||
}
|
}
|
||||||
public static FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex) {
|
public static SwedishGenerator.FillResult fillMask(SwedishGenerator.Rng rng, Grid mask, DictEntry[] dictIndex) {
|
||||||
val multiThreaded = Thread.currentThread().getName().contains("pool");
|
val multiThreaded = Thread.currentThread().getName().contains("pool");
|
||||||
val NO_LOG = (!Main.VERBOSE || multiThreaded);
|
val NO_LOG = (!Main.VERBOSE || multiThreaded);
|
||||||
val grid = mask.deepCopyGrid();
|
val grid = mask.deepCopyGrid();
|
||||||
@@ -885,7 +971,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
System.out.print("\r" + Strings.padRight(msg, 120));
|
System.out.print("\r" + Strings.padRight(msg, 120));
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
}
|
}
|
||||||
Pick chooseMRV() {
|
SwedishGenerator.Pick chooseMRV() {
|
||||||
Slot best = null;
|
Slot best = null;
|
||||||
for (int i = 0, count, count2 = -1, bestScore = -1, n = TOTAL; i < n; i++) {
|
for (int i = 0, count, count2 = -1, bestScore = -1, n = TOTAL; i < n; i++) {
|
||||||
var s = slots[i];
|
var s = slots[i];
|
||||||
@@ -993,7 +1079,7 @@ public record SwedishGenerator(Rng rng, int[] stack) {
|
|||||||
var ok = solver.backtrack(0);
|
var ok = solver.backtrack(0);
|
||||||
// final progress line
|
// final progress line
|
||||||
|
|
||||||
var res = new FillResult(ok, new Gridded(grid), assigned, new FillStats(solver.nodes, solver.backtracks, (System.currentTimeMillis() - t0) / 1000.0, solver.lastMRV));
|
var res = new FillResult(ok, new Gridded(grid), assigned, new SwedishGenerator.FillStats(solver.nodes, solver.backtracks, (System.currentTimeMillis() - t0) / 1000.0, solver.lastMRV));
|
||||||
if (!multiThreaded) {
|
if (!multiThreaded) {
|
||||||
System.out.print("\r" + Strings.padRight("", 120) + "\r");
|
System.out.print("\r" + Strings.padRight("", 120) + "\r");
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ public class SwedishGeneratorTest {
|
|||||||
@Test
|
@Test
|
||||||
void testMaskFitnessBasic() {
|
void testMaskFitnessBasic() {
|
||||||
var gen = new SwedishGenerator(new Rng(0), new int[STACK_SIZE]);
|
var gen = new SwedishGenerator(new Rng(0), new int[STACK_SIZE]);
|
||||||
var grid = Grid.createEmpty();
|
var grid = Clues.createEmpty();
|
||||||
// Empty grid should have high penalty (no slots)
|
// Empty grid should have high penalty (no slots)
|
||||||
var f1 = gen.maskFitness(grid);
|
var f1 = gen.maskFitness(grid);
|
||||||
assertTrue(f1 >= 1_000_000_000L);
|
assertTrue(f1 >= 1_000_000_000L);
|
||||||
@@ -426,7 +426,7 @@ public class SwedishGeneratorTest {
|
|||||||
@Test
|
@Test
|
||||||
void testMaskFitnessDetailed() {
|
void testMaskFitnessDetailed() {
|
||||||
var gen = new SwedishGenerator(new Rng(42), new int[STACK_SIZE]);
|
var gen = new SwedishGenerator(new Rng(42), new int[STACK_SIZE]);
|
||||||
var grid = Grid.createEmpty();
|
var grid = Clues.createEmpty();
|
||||||
// Empty grid: huge penalty
|
// Empty grid: huge penalty
|
||||||
var fitEmpty = gen.maskFitness(grid);
|
var fitEmpty = gen.maskFitness(grid);
|
||||||
assertTrue(fitEmpty >= 1_000_000_000L);
|
assertTrue(fitEmpty >= 1_000_000_000L);
|
||||||
|
|||||||
Reference in New Issue
Block a user